feat: add shipping service documentation and update seller dashboard pages and api services
This commit is contained in:
parent
de8873859e
commit
42816905c4
12 changed files with 7469 additions and 321 deletions
563
documentation/forward_integration.md
Normal file
563
documentation/forward_integration.md
Normal file
|
|
@ -0,0 +1,563 @@
|
|||
# Shadowfax Unified API (SFX) — Complete Forward Integration Document
|
||||
|
||||
> **Document Scope**: End-to-End Technical Integration Guide for Shadowfax Logistics Platform
|
||||
> **Supported Models**: Marketplace Seller Pickup & Warehouse Hub Dispatch
|
||||
> **API Version**: Unified API v1 / v3 / v4
|
||||
> **Document Status**: Production Ready
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary & Architecture
|
||||
|
||||
Shadowfax (SFX) Unified API offers a single, standardized REST interface designed to streamline forward logistics operations across e-commerce marketplaces, brand storefronts, quick-commerce networks, and regional fulfillment hubs. The platform automates every stage of the shipment lifecycle—from pincode serviceability and pre-allocated AWB numbers to multi-location order placement, real-time shipment tracking, active escalation handling, digital proof of delivery (POD), dynamic COD QR codes, and real-time webhook callback notifications.
|
||||
|
||||
### Operational Models Breakdown
|
||||
1. **Marketplace Model (Seller Pickup & Delivery)**:
|
||||
- **First Mile**: Shadowfax delivery partners pick up shipments directly from distributed seller stores, retail outlets, or micro-warehouses.
|
||||
- **Middle Mile**: Shipments are routed via local Origin Hubs and linehaul Distribution Centers (DCs).
|
||||
- **Last Mile**: Delivered to the end customer address.
|
||||
- **Reverse / RTS**: If delivery fails after max attempts or customer rejection, the order triggers Return to Seller (RTS) and is returned to the origin pickup location.
|
||||
2. **Warehouse Model (Hub Dispatch & Delivery)**:
|
||||
- **First Mile / Linehaul**: Shipments originate directly from centralized client warehouses or regional distribution hubs and are handed over to Shadowfax.
|
||||
- **Last Mile**: Processed through Destination Hubs and delivered to the end customer.
|
||||
- **Reverse / RTO**: If undeliverable, shipments enter Return to Origin (RTO) and return to the client warehouse.
|
||||
|
||||
### Environments & Host URLs
|
||||
| Environment | Base URL | Purpose |
|
||||
| :--- | :--- | :--- |
|
||||
| **Staging / Sandbox** | `https://dale.staging.shadowfax.in` | Developer integration, QA testing & test pincodes (`110009`, `560077`, `560007`) |
|
||||
| **Production** | `https://flash.shadowfax.in` | Live order processing & operational delivery management |
|
||||
|
||||
---
|
||||
|
||||
## 2. Technical Glossary & Core Logistics Concepts
|
||||
|
||||
### Facility Network Architecture
|
||||
- **Distribution Center (DC)**: Large sorting facilities processing bulk intracity and intercity linehaul shipments.
|
||||
- **Hub**: Neighborhood-level delivery centers managing rider assignment for last-mile delivery and first-mile seller pickup.
|
||||
- **Origin Hub**: The initial Shadowfax facility receiving packages directly from sellers or micro-warehouses.
|
||||
- **Origin DC**: The regional distribution center receiving packages from Origin Hubs or client primary warehouses.
|
||||
- **Destination DC**: The sorting hub in the recipient city prior to local hub distribution.
|
||||
- **Destination Hub**: The final delivery hub from which last-mile riders execute customer delivery.
|
||||
- **Via DC**: Intermediate transit sorting hubs where manifest bags pass through without unbagging.
|
||||
|
||||
### Logistics Identifiers & Artifacts
|
||||
- **Air Waybill (AWB)**: Unique 10-14 digit tracking identification number assigned to every parcel.
|
||||
- **Letter of Receipt (LOR)**: Formal digital acknowledgement issued during physical handover of bulk parcels.
|
||||
- **Proof of Delivery (POD)**: Digital evidence (recipient signature, photo timestamp, or OTP) confirming delivery.
|
||||
- **COD / Cash on Delivery**: Collect-on-delivery payments collected via cash or dynamic UPI QR codes.
|
||||
|
||||
---
|
||||
|
||||
## 3. Authentication & Global HTTP Specifications
|
||||
|
||||
Shadowfax API uses **HTTP Header Token Authentication**. Production and Staging API tokens are provisioned by the Shadowfax Integration Team.
|
||||
|
||||
### Mandatory Request Headers
|
||||
```http
|
||||
Authorization: Token <YOUR_PROVISIONED_API_TOKEN>
|
||||
Content-Type: application/json
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
### Standard HTTP Status Codes
|
||||
| HTTP Code | Category | Meaning & Operational Handling |
|
||||
| :--- | :--- | :--- |
|
||||
| `200 OK` | Success | Request succeeded. Response body contains order status or payload data. |
|
||||
| `201 Created` | Success | Order created or AWB generated successfully. |
|
||||
| `400 Bad Request` | Client Error | Schema validation failure or missing mandatory fields. Check field types. |
|
||||
| `401 Unauthorized` | Security Error | Missing or invalid API Token in `Authorization` header. |
|
||||
| `404 Not Found` | Resource Error | AWB number or Order ID does not exist in Shadowfax system. |
|
||||
| `422 Unprocessable` | Business Error | Non-serviceable pincode, duplicate `client_order_id`, or invalid state transition. |
|
||||
| `500 Server Error` | System Error | Downstream system unavailable; execute exponential backoff retry. |
|
||||
|
||||
---
|
||||
|
||||
## 4. Order Lifecycle & State Transitions
|
||||
|
||||
### 4.1 Marketplace Order Lifecycle States
|
||||
| State Name | Status Description | Next Valid States |
|
||||
| :--- | :--- | :--- |
|
||||
| `new` | Order created in Shadowfax system; awaiting seller pickup assignment. | `pickup_assigned`, `cancelled` |
|
||||
| `pickup_assigned` | Rider assigned for seller pickup. | `pickup_rescheduled`, `picked_up`, `pickup_cancelled` |
|
||||
| `pickup_rescheduled` | Seller requested pickup deferral / pickup hold. | `pickup_assigned`, `pickup_cancelled` |
|
||||
| `picked_up` | Package collected from seller; in transit to Origin Hub. | `in_transit` |
|
||||
| `in_transit` | Moving between Origin Hub, DCs, and Destination Hub. | `out_for_delivery` |
|
||||
| `out_for_delivery` | Package with last-mile delivery rider for customer attempt. | `delivered`, `delivery_rescheduled`, `rts_initiated` |
|
||||
| `delivery_rescheduled` | Delivery attempt failed or customer requested date change. | `out_for_delivery`, `rts_initiated` |
|
||||
| `delivered` | Package successfully delivered to customer. | *Terminal State* |
|
||||
| `cancelled` | Order cancelled prior to pickup. | *Terminal State* |
|
||||
| `rts_initiated` | Return to Seller initiated after max delivery attempts or rejection. | `rts_in_transit` |
|
||||
| `rts_in_transit` | Package returning to seller location. | `rts_out_for_delivery` |
|
||||
| `rts_out_for_delivery` | Rider out to hand over returned package back to seller. | `rts_delivered` |
|
||||
| `rts_delivered` | Package successfully handed back to seller. | *Terminal State* |
|
||||
|
||||
### 4.2 Warehouse Order Lifecycle States
|
||||
| State Name | Status Description | Next Valid States |
|
||||
| :--- | :--- | :--- |
|
||||
| `new` | Order created for warehouse fulfillment. | `in_transit`, `cancelled` |
|
||||
| `in_transit` | In transit from client warehouse to destination hub. | `out_for_delivery` |
|
||||
| `out_for_delivery` | Package out for customer delivery attempt. | `delivered`, `delivery_rescheduled`, `rto_initiated` |
|
||||
| `delivery_rescheduled` | Customer requested alternate delivery slot or delivery hold. | `out_for_delivery`, `rto_initiated` |
|
||||
| `delivered` | Successfully delivered to customer. | *Terminal State* |
|
||||
| `cancelled` | Order cancelled before dispatch. | *Terminal State* |
|
||||
| `rto_initiated` | Return to Origin initiated. | `rto_in_transit` |
|
||||
| `rto_in_transit` | Package moving back to client warehouse. | `rto_delivered` |
|
||||
| `rto_delivered` | Package returned and verified at client warehouse. | *Terminal State* |
|
||||
|
||||
---
|
||||
|
||||
## 5. Push Callback API (Webhooks)
|
||||
|
||||
Shadowfax provides real-time event notifications via HTTP POST webhooks pushed directly to the client's configured endpoint whenever a shipment transitions state.
|
||||
|
||||
### Webhook Request Headers
|
||||
```http
|
||||
Content-Type: application/json
|
||||
X-Shadowfax-Signature: <HMAC_SHA256_HASH>
|
||||
```
|
||||
|
||||
### Webhook Standard Payload Schema
|
||||
```json
|
||||
{
|
||||
"client_order_id": "ORD-2026-99012",
|
||||
"awb_number": "SFX10928374651",
|
||||
"status": "out_for_delivery",
|
||||
"sub_status": "out_for_delivery",
|
||||
"remarks": "Package out for delivery with rider Rajesh (Ph: +919876543210)",
|
||||
"reason_code": "",
|
||||
"timestamp": "2026-09-25T14:30:00Z",
|
||||
"location": {
|
||||
"latitude": 12.9716,
|
||||
"longitude": 77.5946,
|
||||
"city": "Bengaluru"
|
||||
},
|
||||
"attempt_count": 1,
|
||||
"pickup_details": {
|
||||
"pickup_id": "PICKUP-8812",
|
||||
"name": "Acme Electronics Hub"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Webhook Event State Mappings
|
||||
| Webhook `status` | Marketplace State Equivalent | Warehouse State Equivalent | Trigger Event |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `pickup_assigned` | `pickup_assigned` | N/A | Pickup executive assigned to seller. |
|
||||
| `picked_up` | `picked_up` | N/A | Package scanned at seller location. |
|
||||
| `in_transit` | `in_transit` | `in_transit` | Package arrived or departed DC/Hub. |
|
||||
| `out_for_delivery` | `out_for_delivery` | `out_for_delivery` | Package scanned onto rider runsheet. |
|
||||
| `delivered` | `delivered` | `delivered` | Successful delivery to customer. |
|
||||
| `delivery_rescheduled`| `delivery_rescheduled`| `delivery_rescheduled`| Delivery attempt deferred with reason code. |
|
||||
| `rts_initiated` | `rts_initiated` | N/A | Return to seller triggered. |
|
||||
| `rto_initiated` | N/A | `rto_initiated` | Return to origin warehouse triggered. |
|
||||
|
||||
---
|
||||
|
||||
## 6. Endpoints Reference & Full API Specifications
|
||||
|
||||
### 6.1 Serviceability API
|
||||
**HTTP Method**: `GET`
|
||||
**Endpoint Path**: `/v1/clients/serviceability/?service={service}&page={page}&count={count}&pincodes={pincodes}`
|
||||
**Description**: Fetches list of serviceable pincodes or verifies if a specific destination pincode is active for delivery/pickup.
|
||||
|
||||
#### Query Parameters
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `service` | String | No | Service filter (e.g. `express`, `surface`, `reverse`). |
|
||||
| `pincodes` | String | No | Comma-separated list of 6-digit Indian pincodes to query. |
|
||||
| `page` | Integer | No | Page number for paginated pincode index (Default: `1`). |
|
||||
| `count` | Integer | No | Number of records per page (Default: `100`, Max: `1000`). |
|
||||
|
||||
#### Sample Response Payload (`200 OK`)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Serviceability retrieved successfully",
|
||||
"data": {
|
||||
"total_count": 2,
|
||||
"page": 1,
|
||||
"pincodes": [
|
||||
{
|
||||
"pincode": "560077",
|
||||
"city": "Bengaluru",
|
||||
"state": "Karnataka",
|
||||
"serviceable": true,
|
||||
"pickup_available": true,
|
||||
"delivery_available": true,
|
||||
"cod_available": true,
|
||||
"prepaid_available": true
|
||||
},
|
||||
{
|
||||
"pincode": "110009",
|
||||
"city": "New Delhi",
|
||||
"state": "Delhi",
|
||||
"serviceable": true,
|
||||
"pickup_available": true,
|
||||
"delivery_available": true,
|
||||
"cod_available": true,
|
||||
"prepaid_available": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 AWB Generation API
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v3/clients/generate_marketplace_awb/`
|
||||
**Description**: Pre-allocates a Shadowfax AWB tracking number prior to creating the complete order payload.
|
||||
|
||||
#### Sample Request Payload
|
||||
```json
|
||||
{
|
||||
"client_code": "ACME_RETAIL",
|
||||
"count": 1
|
||||
}
|
||||
```
|
||||
|
||||
#### Sample Response Payload (`200 OK`)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"awb_numbers": [
|
||||
"SFX9928374102"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 Marketplace Order Creation API (Seller Pickup Model)
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v3/clients/orders/`
|
||||
**Description**: Creates a seller pickup request and customer delivery order in a single API call.
|
||||
|
||||
#### Request Payload Parameter Matrix
|
||||
##### 1. `order_details` Object
|
||||
| Field Name | Type | Required | Validation & Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `client_order_id` | String | **Yes** | Client internal order ID (Must be unique across system). |
|
||||
| `awb_number` | String | No | Pre-generated Shadowfax AWB number. Auto-generated if omitted. |
|
||||
| `actual_weight` | Number | **Yes** | Weight of shipment in grams (e.g. `500.0`). |
|
||||
| `volumetric_weight` | Number | No | Volumetric weight in grams `(L * W * H) / 5000` (Default: `0.0`). |
|
||||
| `product_value` | Number | **Yes** | Total invoice product value in INR excluding tax. |
|
||||
| `payment_mode` | String | **Yes** | Payment type: `"Prepaid"` or `"COD"`. |
|
||||
| `cod_amount` | Number | **Yes** | COD amount to collect from recipient (`0` if `Prepaid`). |
|
||||
| `promised_delivery_date` | String | No | Expected delivery date format `YYYY-MM-DD`. |
|
||||
|
||||
##### 2. `customer_details` Object
|
||||
| Field Name | Type | Required | Validation & Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `name` | String | **Yes** | Recipient full name. |
|
||||
| `phone` | String | **Yes** | 10-digit mobile number of recipient. |
|
||||
| `address_line_1` | String | **Yes** | Building, street, flat number. |
|
||||
| `address_line_2` | String | No | Landmark, locality. |
|
||||
| `pincode` | String | **Yes** | 6-digit serviceable delivery pincode. |
|
||||
| `city` | String | **Yes** | Destination city. |
|
||||
| `state` | String | **Yes** | Destination state. |
|
||||
|
||||
##### 3. `pickup_details` Object
|
||||
| Field Name | Type | Required | Validation & Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `name` | String | **Yes** | Seller / Store / Warehouse name. |
|
||||
| `phone` | String | **Yes** | 10-digit contact mobile number of seller. |
|
||||
| `address_line_1` | String | **Yes** | Seller warehouse address. |
|
||||
| `pincode` | String | **Yes** | 6-digit origin pickup pincode. |
|
||||
| `city` | String | **Yes** | Origin city. |
|
||||
| `state` | String | **Yes** | Origin state. |
|
||||
|
||||
##### 4. `rts_details` Object (Return to Seller)
|
||||
| Field Name | Type | Required | Validation & Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `name` | String | **Yes** | Return contact name. |
|
||||
| `phone` | String | **Yes** | Return contact phone number. |
|
||||
| `address_line_1` | String | **Yes** | Return seller address. |
|
||||
| `pincode` | String | **Yes** | 6-digit return pincode. |
|
||||
|
||||
##### 5. `product_details` Array Items
|
||||
| Field Name | Type | Required | Validation & Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `sku_name` | String | **Yes** | Item title or product name. |
|
||||
| `sku_code` | String | **Yes** | Merchant SKU code. |
|
||||
| `price` | Number | **Yes** | Individual SKU price. |
|
||||
| `quantity` | Integer | **Yes** | Quantity of items. |
|
||||
|
||||
#### Complete Marketplace Request Payload Example
|
||||
```json
|
||||
{
|
||||
"order_type": "marketplace",
|
||||
"order_details": {
|
||||
"client_order_id": "ORD-MKT-2026-101",
|
||||
"awb_number": "SFX9928374102",
|
||||
"actual_weight": 750.0,
|
||||
"volumetric_weight": 800.0,
|
||||
"product_value": 1499.0,
|
||||
"payment_mode": "COD",
|
||||
"cod_amount": 1499.0
|
||||
},
|
||||
"customer_details": {
|
||||
"name": "Ananya Sharma",
|
||||
"phone": "9876543210",
|
||||
"address_line_1": "Flat 402, Sunshine Apartments",
|
||||
"address_line_2": "Indiranagar 10th Main",
|
||||
"pincode": "560038",
|
||||
"city": "Bengaluru",
|
||||
"state": "Karnataka"
|
||||
},
|
||||
"pickup_details": {
|
||||
"name": "StyleHub Retail Store",
|
||||
"phone": "9123456789",
|
||||
"address_line_1": "Plot 12, Industrial Area Phase 2",
|
||||
"pincode": "560077",
|
||||
"city": "Bengaluru",
|
||||
"state": "Karnataka"
|
||||
},
|
||||
"rts_details": {
|
||||
"name": "StyleHub Returns Center",
|
||||
"phone": "9123456789",
|
||||
"address_line_1": "Plot 12, Industrial Area Phase 2",
|
||||
"pincode": "560077",
|
||||
"city": "Bengaluru",
|
||||
"state": "Karnataka"
|
||||
},
|
||||
"product_details": [
|
||||
{
|
||||
"sku_name": "Wireless Noise Cancelling Earbuds",
|
||||
"sku_code": "SKU-EAR-001",
|
||||
"price": 1499.0,
|
||||
"quantity": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Sample Response Payload (`201 Created`)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Order created successfully",
|
||||
"data": {
|
||||
"client_order_id": "ORD-MKT-2026-101",
|
||||
"awb_number": "SFX9928374102",
|
||||
"order_status": "new",
|
||||
"created_at": "2026-09-25T14:35:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 Warehouse Order Creation API (Hub Dispatch Model)
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v3/clients/orders/`
|
||||
**Description**: Creates a delivery order for shipments dispatched directly from a client warehouse or hub.
|
||||
|
||||
#### Complete Warehouse Request Payload Example
|
||||
```json
|
||||
{
|
||||
"order_type": "warehouse",
|
||||
"order_details": {
|
||||
"client_order_id": "ORD-WH-2026-505",
|
||||
"awb_number": "SFX8817263541",
|
||||
"actual_weight": 1200.0,
|
||||
"product_value": 3500.0,
|
||||
"payment_mode": "Prepaid",
|
||||
"cod_amount": 0.0
|
||||
},
|
||||
"customer_details": {
|
||||
"name": "Vikram Mehta",
|
||||
"phone": "9988776655",
|
||||
"address_line_1": "House 14, Civil Lines",
|
||||
"pincode": "110054",
|
||||
"city": "New Delhi",
|
||||
"state": "Delhi"
|
||||
},
|
||||
"rto_details": {
|
||||
"name": "Central Warehouse North",
|
||||
"phone": "9811002233",
|
||||
"address_line_1": "Warehouse Complex Hub 3",
|
||||
"pincode": "110042",
|
||||
"city": "New Delhi",
|
||||
"state": "Delhi"
|
||||
},
|
||||
"product_details": [
|
||||
{
|
||||
"sku_name": "Mechanical Keyboard RGB",
|
||||
"sku_code": "SKU-KB-102",
|
||||
"price": 3500.0,
|
||||
"quantity": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 6.5 Single Order Tracking API (V4)
|
||||
**HTTP Method**: `GET`
|
||||
**Endpoint Path**: `/v4/clients/orders/{awb_number}/track/`
|
||||
**Description**: Retrieves comprehensive tracking timeline, current location, rider details, and status for a shipment.
|
||||
|
||||
#### Sample Response Payload (`200 OK`)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"client_order_id": "ORD-MKT-2026-101",
|
||||
"awb_number": "SFX9928374102",
|
||||
"current_status": "out_for_delivery",
|
||||
"expected_delivery_date": "2026-09-25",
|
||||
"rider_details": {
|
||||
"name": "Rajesh Kumar",
|
||||
"phone": "+919876543210"
|
||||
},
|
||||
"tracking_history": [
|
||||
{
|
||||
"status": "new",
|
||||
"location": "Bengaluru Hub",
|
||||
"timestamp": "2026-09-24T10:00:00Z",
|
||||
"remarks": "Order created"
|
||||
},
|
||||
{
|
||||
"status": "picked_up",
|
||||
"location": "StyleHub Store",
|
||||
"timestamp": "2026-09-24T14:30:00Z",
|
||||
"remarks": "Package picked up from seller"
|
||||
},
|
||||
{
|
||||
"status": "in_transit",
|
||||
"location": "Bengaluru Main DC",
|
||||
"timestamp": "2026-09-24T19:00:00Z",
|
||||
"remarks": "Arrived at sorting DC"
|
||||
},
|
||||
{
|
||||
"status": "out_for_delivery",
|
||||
"location": "Indiranagar Hub",
|
||||
"timestamp": "2026-09-25T08:30:00Z",
|
||||
"remarks": "Assigned to rider for delivery"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.6 Bulk Order Tracking API (V4)
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v4/clients/bulk_track/`
|
||||
**Description**: Fetches current tracking status for up to 50 AWB numbers in a single request.
|
||||
|
||||
#### Sample Request Payload
|
||||
```json
|
||||
{
|
||||
"awb_numbers": [
|
||||
"SFX9928374102",
|
||||
"SFX8817263541"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 6.7 Order Cancellation API
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v3/clients/orders/cancel/`
|
||||
**Description**: Cancels a pending order prior to rider pickup or warehouse dispatch.
|
||||
|
||||
#### Sample Request Payload
|
||||
```json
|
||||
{
|
||||
"client_order_id": "ORD-MKT-2026-101",
|
||||
"awb_number": "SFX9928374102",
|
||||
"reason": "Customer requested cancellation"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.8 Escalation & Support Ticket API
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v1/clients/support/issue/`
|
||||
**Description**: Raises operational support tickets directly to Shadowfax ground control team.
|
||||
|
||||
#### Issue Categories Available
|
||||
- `DELAYED_DELIVERY`: Delivery SLA breached or delayed.
|
||||
- `EXPEDITE_PICKUP`: Urgent seller pickup required.
|
||||
- `DELIVERY_DISPUTE`: Customer disputes status marked delivered.
|
||||
- `ADDRESS_UPDATE`: Correcting destination address/phone.
|
||||
|
||||
#### Sample Request Payload
|
||||
```json
|
||||
{
|
||||
"awb_number": "SFX9928374102",
|
||||
"category": "EXPEDITE_PICKUP",
|
||||
"description": "Seller store closing early today at 4 PM, please pick up by 3 PM."
|
||||
}
|
||||
```
|
||||
|
||||
### 6.9 Proof of Delivery (POD) API
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v1/clients/pod_details/`
|
||||
**Description**: Retrieves digital signature image URL, photo proof, or OTP verification details for delivered shipments.
|
||||
|
||||
#### Sample Response Payload (`200 OK`)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"awb_number": "SFX9928374102",
|
||||
"delivery_timestamp": "2026-09-25T11:45:00Z",
|
||||
"signature_url": "https://cdn.shadowfax.in/pod/signatures/SFX9928374102.png",
|
||||
"photo_proof_url": "https://cdn.shadowfax.in/pod/photos/SFX9928374102.jpg",
|
||||
"receiver_name": "Ananya Sharma",
|
||||
"otp_verified": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.10 Dynamic QR Code Generation API (V2)
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v2/clients/generate_qr_code/`
|
||||
**Description**: Generates dynamic UPI payment QR code strings for Cash-on-Delivery collection at customer doorstep.
|
||||
|
||||
#### Sample Response Payload (`200 OK`)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"awb_number": "SFX9928374102",
|
||||
"cod_amount": 1499.0,
|
||||
"qr_code_string": "upi://pay?pa=shadowfax@icici&pn=Shadowfax&am=1499.00&tr=SFX9928374102&tn=COD%20Payment",
|
||||
"qr_code_image_base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Appendix: Standard Reason & Remark Codes Catalog
|
||||
|
||||
Shadowfax uses standardized numerical and string remark codes during exception events. Client system webhooks should parse these codes for automated business logic.
|
||||
|
||||
### 7.1 Delivery On-Hold Remark Codes
|
||||
| Reason Code | Code Description | Suggested Operational Handling |
|
||||
| :--- | :--- | :--- |
|
||||
| `OH_ADDR_INCOMPLETE` | Customer address incomplete or missing landmark. | Trigger SMS/WhatsApp to customer to update address. |
|
||||
| `OH_CUST_UNAVAILABLE` | Customer not available at address. | Reschedule for next business day automatically. |
|
||||
| `OH_COD_NOT_READY` | Customer requested delivery when cash/UPI ready. | Reschedule delivery date per customer request. |
|
||||
| `OH_PINCODE_UNMAPPED` | Destination pincode out of delivery hub zone. | Trigger escalation ticket to reassign hub. |
|
||||
|
||||
### 7.2 Pickup On-Hold Remark Codes
|
||||
| Reason Code | Code Description | Suggested Operational Handling |
|
||||
| :--- | :--- | :--- |
|
||||
| `PU_STORE_CLOSED` | Seller store/warehouse closed during pickup attempt. | Notify seller contact via alert. |
|
||||
| `PU_ITEM_NOT_READY` | Shipment packaging incomplete at seller end. | Reschedule seller pickup window. |
|
||||
| `PU_ADDR_NOT_FOUND` | Seller pickup address untraceable. | Verify pickup coordinates and phone number. |
|
||||
|
||||
### 7.3 Cancellation Reason Codes
|
||||
| Reason Code | Code Description | Originator |
|
||||
| :--- | :--- | :--- |
|
||||
| `CAN_CLIENT_REQ` | Order cancelled by client system via API. | Client |
|
||||
| `CAN_SELLER_DENIED` | Seller denied stock availability during pickup. | Seller |
|
||||
| `CAN_EXCEEDED_ATTEMPTS`| Max delivery attempts reached (RTS/RTO triggered).| Courier |
|
||||
|
||||
---
|
||||
|
||||
## 8. Summary & Next Steps for Integration
|
||||
|
||||
1. **Credential Setup**: Obtain Staging API Token and test pincode list from Shadowfax Integration Team.
|
||||
2. **Webhook Endpoint Registration**: Configure HTTPS POST Webhook listener and verify HMAC signature parsing.
|
||||
3. **End-to-End Sandbox Testing**: Execute Pincode Check $\rightarrow$ Order Creation $\rightarrow$ Tracking $\rightarrow$ Webhook Receive $\rightarrow$ POD retrieval flow.
|
||||
4. **Go-Live Readiness Review**: Obtain sign-off from Shadowfax Logistics Operations for Production URL switch.
|
||||
418
documentation/reverse_integration.md
Normal file
418
documentation/reverse_integration.md
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
# Shadowfax Reverse Pickup API (SFX) — Complete Reverse Integration Document
|
||||
|
||||
> **Document Scope**: Technical Specification & Integration Guide for Shadowfax Reverse Logistics & Customer Return Platform
|
||||
> **Core Capability**: Doorstep Pickups, Quality Check (QC) Enforcement, Warehouse Returns & Callback Synchronization
|
||||
> **API Version**: Reverse API v1 / v2 / v3 / v4
|
||||
> **Document Status**: Production Ready
|
||||
|
||||
---
|
||||
|
||||
## 1. System Architecture & Reverse Logistics Overview
|
||||
|
||||
The Shadowfax (SFX) Reverse Pickup API enables e-commerce merchants, marketplace operators, and brand retailers to automate customer return pickups and doorstep Quality Checks (QC). Unlike forward delivery where parcels originate from seller/warehouse locations, reverse logistics manages customer doorstep pickups, item inspection, hub consolidation, linehaul transport, and final delivery back to the client's return warehouse.
|
||||
|
||||
### Key Operational Capabilities
|
||||
1. **Doorstep Customer Pickup**: Delivery executive collects items directly from the customer's specified pickup address.
|
||||
2. **Configurable Quality Check (QC)**: Executive verifies physical item condition, brand, serial/IMEI numbers, original packaging, and tags prior to accept/reject.
|
||||
3. **Reverse Linehaul & Consolidation**: Collected items are routed through Shadowfax Origin Hubs and linehaul Distribution Centers (DCs).
|
||||
4. **Warehouse Return Delivery**: Final delivery and digital handover of collected return inventory to client fulfillment centers.
|
||||
5. **Real-time Callback Synchronization**: Push webhooks notify client systems instantly upon pickup status change, QC pass/fail events, and warehouse delivery.
|
||||
|
||||
### Environment Base URLs
|
||||
| Environment | Base URL | Purpose |
|
||||
| :--- | :--- | :--- |
|
||||
| **Staging / Sandbox** | `https://dale.staging.shadowfax.in` | Integration testing & test pincodes (`110009`, `560077`, `560007`) |
|
||||
| **Production** | `https://flash.shadowfax.in` | Live reverse order creation & operational execution |
|
||||
|
||||
---
|
||||
|
||||
## 2. Technical Glossary & Reverse Network Facilities
|
||||
|
||||
### Facility Network Architecture
|
||||
- **Pickup Customer Location**: The customer residential or commercial address where the return item is collected.
|
||||
- **Origin Hub**: First Shadowfax local hub receiving the picked-up return item from the delivery rider.
|
||||
- **Origin DC**: Primary sorting Distribution Center processing reverse linehaul routing.
|
||||
- **Destination DC**: Sorting center located near the client return warehouse.
|
||||
- **Destination Hub / Return Hub**: Final Shadowfax hub executing bulk delivery back to the client warehouse.
|
||||
- **Client Return Warehouse**: Final destination facility where returned goods are handed over to the merchant.
|
||||
|
||||
### Reverse Logistics Identifiers
|
||||
- **Client Request ID / Order ID**: Client's unique internal reference for the return request.
|
||||
- **Reverse Air Waybill (AWB)**: Unique tracking number assigned to the reverse shipment.
|
||||
- **QC Parameter**: Inspection rules (e.g., brand check, IMEI match, unused status) defined per item.
|
||||
- **Proof of Pickup (POP) / POD**: Digital signature or photo timestamp confirming return item collection.
|
||||
|
||||
---
|
||||
|
||||
## 3. Authentication & Global HTTP Specifications
|
||||
|
||||
Shadowfax API uses **HTTP Header Token Authentication**. Production and Staging API tokens are provisioned by the Shadowfax Integration Team.
|
||||
|
||||
### Mandatory HTTP Headers
|
||||
```http
|
||||
Authorization: Token <YOUR_PROVISIONED_API_TOKEN>
|
||||
Content-Type: application/json
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
### Standard HTTP Status Codes
|
||||
| HTTP Code | Category | Operational Meaning & Action |
|
||||
| :--- | :--- | :--- |
|
||||
| `200 OK` | Success | Request succeeded. Body contains request details or query results. |
|
||||
| `201 Created` | Success | Reverse pickup request successfully created. |
|
||||
| `400 Bad Request` | Validation Error | Missing mandatory field or invalid data type format. |
|
||||
| `401 Unauthorized` | Security Error | Invalid or missing API token in `Authorization` header. |
|
||||
| `404 Not Found` | Resource Error | Client Request ID or Reverse AWB number does not exist. |
|
||||
| `422 Unprocessable` | Business Rule | Non-serviceable pickup pincode or duplicate `client_request_id`. |
|
||||
| `500 Server Error` | System Exception | Downstream system unavailable; execute retry with exponential backoff. |
|
||||
|
||||
---
|
||||
|
||||
## 4. Reverse Order Lifecycle & Quality Check (QC) Architecture
|
||||
|
||||
### 4.1 Reverse Pickup Lifecycle States
|
||||
| State Name | Status Description | Next Valid States |
|
||||
| :--- | :--- | :--- |
|
||||
| `new` | Pickup request created in Shadowfax system; awaiting rider assignment. | `pickup_assigned`, `cancelled` |
|
||||
| `pickup_assigned` | Rider assigned to visit customer address. | `picked_up`, `qc_failed`, `pickup_rescheduled`, `cancelled` |
|
||||
| `pickup_rescheduled` | Customer requested alternate pickup date/time window. | `pickup_assigned`, `cancelled` |
|
||||
| `picked_up` | Item passed QC checks and collected from customer; in transit to Origin Hub. | `in_transit` |
|
||||
| `qc_failed` | Item failed doorstep QC checks (e.g. wrong brand, damaged item). Pickup rejected. | *Terminal State* |
|
||||
| `in_transit` | Item moving between Shadowfax Hubs and linehaul DCs toward client warehouse. | `out_for_delivery_to_warehouse` |
|
||||
| `out_for_delivery_to_warehouse` | Item assigned to rider for bulk handover at client return warehouse. | `delivered_to_warehouse`, `delivery_rescheduled` |
|
||||
| `delivered_to_warehouse` | Return item verified and received at client warehouse. | *Terminal State* |
|
||||
| `cancelled` | Reverse pickup request cancelled prior to pickup execution. | *Terminal State* |
|
||||
|
||||
### 4.2 Quality Check (QC) Enforcement Rules
|
||||
Shadowfax riders perform doorstep QC based on criteria supplied in the order payload:
|
||||
1. **Brand & Model Match**: Verifies item brand name and model match order manifest.
|
||||
2. **Serial / IMEI Verification**: Matches 15-digit mobile IMEI or product serial number.
|
||||
3. **Physical Damage Inspection**: Checks for cracks, dents, water damage, or missing components.
|
||||
4. **Tag / Packaging Verification**: Confirms original brand tags/box are intact if required.
|
||||
5. **Doorstep Image Capture**: Rider uploads photo proof of item condition during pickup.
|
||||
|
||||
---
|
||||
|
||||
## 5. Push Callback API (Webhooks)
|
||||
|
||||
Shadowfax notifies client systems in real time whenever a reverse pickup transitions state.
|
||||
|
||||
### Webhook Request Headers
|
||||
```http
|
||||
Content-Type: application/json
|
||||
X-Shadowfax-Signature: <HMAC_SHA256_HASH>
|
||||
```
|
||||
|
||||
### Webhook Standard Payload Schema
|
||||
```json
|
||||
{
|
||||
"client_request_id": "REV-ORD-2026-8801",
|
||||
"awb_number": "SFX-REV-99182736",
|
||||
"status": "picked_up",
|
||||
"sub_status": "qc_passed",
|
||||
"remarks": "Item passed physical QC and picked up by rider Ramesh",
|
||||
"reason_code": "",
|
||||
"qc_result": {
|
||||
"qc_passed": true,
|
||||
"brand_matched": true,
|
||||
"imei_matched": true,
|
||||
"serial_number": "SN-9988112233",
|
||||
"images": [
|
||||
"https://cdn.shadowfax.in/qc/photos/REV-ORD-2026-8801_1.jpg"
|
||||
]
|
||||
},
|
||||
"timestamp": "2026-09-25T15:10:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. API Endpoints Reference & Complete Technical Specifications
|
||||
|
||||
### 6.1 Reverse Pincode Serviceability API
|
||||
**HTTP Method**: `GET`
|
||||
**Endpoint Path**: `/v1/clients/serviceability/?service=reverse&page={page}&count={count}&pincodes={pincodes}`
|
||||
**Description**: Verifies if customer pickup pincodes are active for reverse pickup operations.
|
||||
|
||||
#### Query Parameters
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `service` | String | **Yes** | Pass `"reverse"` for reverse pickup serviceability query. |
|
||||
| `pincodes` | String | No | Comma-separated 6-digit Indian pincodes to check. |
|
||||
| `page` | Integer | No | Page number for paginated results (Default: `1`). |
|
||||
| `count` | Integer | No | Records per page (Default: `100`). |
|
||||
|
||||
#### Sample Response Payload (`200 OK`)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"total_count": 1,
|
||||
"pincodes": [
|
||||
{
|
||||
"pincode": "560077",
|
||||
"city": "Bengaluru",
|
||||
"state": "Karnataka",
|
||||
"reverse_pickup_available": true,
|
||||
"max_weight_grams": 5000.0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Reverse AWB Generation API
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v3/clients/orders/generate_awb/`
|
||||
**Description**: Pre-allocates tracking numbers for reverse pickup shipments.
|
||||
|
||||
#### Sample Request Payload
|
||||
```json
|
||||
{
|
||||
"client_code": "ACME_RETAIL",
|
||||
"count": 1
|
||||
}
|
||||
```
|
||||
|
||||
#### Sample Response Payload (`200 OK`)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"awb_numbers": [
|
||||
"SFX-REV-99182736"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 Reverse Pickup Request Creation API
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v3/clients/requests`
|
||||
**Description**: Creates a customer doorstep pickup request with embedded QC parameters.
|
||||
|
||||
#### Request Payload Parameter Matrix
|
||||
##### 1. Root Level & Order Identification
|
||||
| Field Name | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `client_request_id` | String | **Yes** | Client internal unique return request identifier. |
|
||||
| `awb_number` | String | No | Pre-allocated reverse AWB number. Auto-generated if omitted. |
|
||||
| `actual_weight` | Number | **Yes** | Expected weight of return item in grams. |
|
||||
| `product_value` | Number | **Yes** | Invoice price of item being returned in INR. |
|
||||
| `pickup_date` | String | No | Scheduled pickup date format `YYYY-MM-DD`. |
|
||||
|
||||
##### 2. `pickup_details` (Customer Location)
|
||||
| Field Name | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `name` | String | **Yes** | Customer full name. |
|
||||
| `phone` | String | **Yes** | 10-digit mobile number of customer. |
|
||||
| `address_line_1` | String | **Yes** | Customer street address and flat/house number. |
|
||||
| `pincode` | String | **Yes** | 6-digit serviceable pickup pincode. |
|
||||
| `city` | String | **Yes** | Pickup city. |
|
||||
| `state` | String | **Yes** | Pickup state. |
|
||||
|
||||
##### 3. `return_details` (Client Warehouse Destination)
|
||||
| Field Name | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `name` | String | **Yes** | Warehouse return hub name. |
|
||||
| `phone` | String | **Yes** | Contact phone number of return warehouse. |
|
||||
| `address_line_1` | String | **Yes** | Warehouse street address. |
|
||||
| `pincode` | String | **Yes** | 6-digit warehouse pincode. |
|
||||
| `city` | String | **Yes** | Warehouse city. |
|
||||
| `state` | String | **Yes** | Warehouse state. |
|
||||
|
||||
##### 4. `qc_details` Object (Quality Check Instructions)
|
||||
| Field Name | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `qc_check` | Boolean | **Yes** | `true` to enforce doorstep QC inspection. |
|
||||
| `brand_name` | String | No | Expected brand name on item label. |
|
||||
| `item_name` | String | **Yes** | SKU name / description. |
|
||||
| `serial_number` | String | No | Expected product serial or IMEI number. |
|
||||
| `image_required` | Boolean | No | `true` if rider must take photo proof during QC. |
|
||||
|
||||
#### Complete Reverse Pickup Request Payload Example
|
||||
```json
|
||||
{
|
||||
"client_request_id": "REV-ORD-2026-8801",
|
||||
"awb_number": "SFX-REV-99182736",
|
||||
"actual_weight": 500.0,
|
||||
"product_value": 2999.0,
|
||||
"pickup_details": {
|
||||
"name": "Kavita Reddy",
|
||||
"phone": "9876501234",
|
||||
"address_line_1": "Apartment 102, Green Glen Layout",
|
||||
"address_line_2": "Bellandur",
|
||||
"pincode": "560103",
|
||||
"city": "Bengaluru",
|
||||
"state": "Karnataka"
|
||||
},
|
||||
"return_details": {
|
||||
"name": "Acme Central Returns Warehouse",
|
||||
"phone": "9811004455",
|
||||
"address_line_1": "Logistics Park Warehouse 4B",
|
||||
"pincode": "560077",
|
||||
"city": "Bengaluru",
|
||||
"state": "Karnataka"
|
||||
},
|
||||
"qc_details": {
|
||||
"qc_check": true,
|
||||
"brand_name": "SoundCore",
|
||||
"item_name": "Over-Ear Headphones Black",
|
||||
"serial_number": "SN-9988112233",
|
||||
"image_required": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Sample Response Payload (`201 Created`)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Reverse pickup request created successfully",
|
||||
"data": {
|
||||
"client_request_id": "REV-ORD-2026-8801",
|
||||
"awb_number": "SFX-REV-99182736",
|
||||
"status": "new",
|
||||
"created_at": "2026-09-25T15:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 Reverse Order Cancellation API
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v2/clients/requests/mark_cancel`
|
||||
**Description**: Cancels a pending customer pickup request before rider execution.
|
||||
|
||||
#### Sample Request Payload
|
||||
```json
|
||||
{
|
||||
"client_request_id": "REV-ORD-2026-8801",
|
||||
"reason": "Customer withdrew return request"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.5 Single Pickup Request Tracking API (V4)
|
||||
**HTTP Method**: `GET`
|
||||
**Endpoint Path**: `/v4/clients/requests/{client_request_id}`
|
||||
**Description**: Retrieves tracking timeline, rider contact, QC status, and location for a reverse pickup.
|
||||
|
||||
#### Sample Response Payload (`200 OK`)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"client_request_id": "REV-ORD-2026-8801",
|
||||
"awb_number": "SFX-REV-99182736",
|
||||
"current_status": "picked_up",
|
||||
"qc_status": "PASSED",
|
||||
"rider_details": {
|
||||
"name": "Ramesh Kumar",
|
||||
"phone": "+919876543210"
|
||||
},
|
||||
"tracking_history": [
|
||||
{
|
||||
"status": "new",
|
||||
"timestamp": "2026-09-25T15:00:00Z",
|
||||
"remarks": "Reverse pickup requested"
|
||||
},
|
||||
{
|
||||
"status": "pickup_assigned",
|
||||
"timestamp": "2026-09-25T15:15:00Z",
|
||||
"remarks": "Assigned to rider Ramesh"
|
||||
},
|
||||
{
|
||||
"status": "picked_up",
|
||||
"timestamp": "2026-09-25T16:00:00Z",
|
||||
"remarks": "Item passed QC check and collected from customer"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.6 Bulk Pickup Request Tracking API (V4)
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v4/clients/requests/bulk_query`
|
||||
**Description**: Batch queries status for up to 50 reverse pickup requests.
|
||||
|
||||
#### Sample Request Payload
|
||||
```json
|
||||
{
|
||||
"client_request_ids": [
|
||||
"REV-ORD-2026-8801",
|
||||
"REV-ORD-2026-8802"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 6.7 Update QC Flag API
|
||||
**HTTP Method**: `PUT`
|
||||
**Endpoint Path**: `/v2/clients/requests/update_qc/`
|
||||
**Description**: Updates QC verification flags or serial number requirements prior to pickup attempt.
|
||||
|
||||
#### Sample Request Payload
|
||||
```json
|
||||
{
|
||||
"client_request_id": "REV-ORD-2026-8801",
|
||||
"qc_check": true,
|
||||
"serial_number": "SN-9988112244"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.8 Proof of Pickup / POD API
|
||||
**HTTP Method**: `GET`
|
||||
**Endpoint Path**: `/v1/clients/pod_details/?client_request_id={client_request_id}`
|
||||
**Description**: Retrieves digital signature image URL and customer OTP verification status for completed pickups.
|
||||
|
||||
#### Sample Response Payload (`200 OK`)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"client_request_id": "REV-ORD-2026-8801",
|
||||
"pickup_timestamp": "2026-09-25T16:00:00Z",
|
||||
"signature_url": "https://cdn.shadowfax.in/pop/signatures/REV-ORD-2026-8801.png",
|
||||
"qc_photos": [
|
||||
"https://cdn.shadowfax.in/qc/photos/REV-ORD-2026-8801_1.jpg"
|
||||
],
|
||||
"otp_verified": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Appendix: Standard Reverse Remark & QC Fail Codes Catalog
|
||||
|
||||
Shadowfax returns standardized remark and reason codes during reverse pickup exceptions.
|
||||
|
||||
### 7.1 Quality Check (QC) Failure Reason Codes
|
||||
| Reason Code | Description | Customer & System Handling |
|
||||
| :--- | :--- | :--- |
|
||||
| `QC_FAIL_BRAND_MISMATCH` | Item brand does not match order manifest. | Re-check customer item or reject return request. |
|
||||
| `QC_FAIL_SERIAL_MISMATCH` | Serial/IMEI number on item differs from manifest. | Verify original order invoice serial number. |
|
||||
| `QC_FAIL_DAMAGED_ITEM` | Item exhibits physical damage not recorded in return request. | Notify merchant customer support. |
|
||||
| `QC_FAIL_MISSING_ACC` | Accessories/chargers missing from return package. | Inform customer to provide complete box contents. |
|
||||
| `QC_FAIL_USED_ITEM` | Item shows clear signs of usage (tags removed). | Reject return per merchant policy. |
|
||||
|
||||
### 7.2 Customer Delay & Intention (CID) Codes
|
||||
| Reason Code | Description | Action |
|
||||
| :--- | :--- | :--- |
|
||||
| `CID_CUST_UNAVAILABLE` | Customer not available at pickup address. | Reschedule for next business day. |
|
||||
| `CID_ITEM_NOT_PACKED` | Customer item not packed or ready for handoff. | Reschedule pickup window. |
|
||||
| `CID_CUST_REFUSED` | Customer changed mind and refused to hand over item. | Trigger cancellation workflow. |
|
||||
|
||||
### 7.3 On-Hold & Unreachable Remark Codes
|
||||
| Reason Code | Description | Action |
|
||||
| :--- | :--- | :--- |
|
||||
| `NC_PHONE_UNREACHABLE` | Rider unable to reach customer phone. | Trigger automated SMS/WhatsApp call-back link. |
|
||||
| `NC_ADDRESS_INCOMPLETE`| Customer address untraceable by rider. | Request customer address update via client UI. |
|
||||
|
||||
---
|
||||
|
||||
## 8. Integration Walkthrough & Next Steps
|
||||
|
||||
1. **API Token Setup**: Request Reverse API Token from Shadowfax Integration Team.
|
||||
2. **Webhook Registration**: Configure HTTPS endpoint for receiving reverse state callbacks (`picked_up`, `qc_failed`, `delivered_to_warehouse`).
|
||||
3. **Sandbox Testing**: Create test pickup request $\rightarrow$ Execute simulated QC pass/fail $\rightarrow$ Receive Webhooks $\rightarrow$ Retrieve POD.
|
||||
4. **Go-Live Readiness**: Transition endpoint URL to Production host `https://flash.shadowfax.in`.
|
||||
1894
documentation/sfx_reverse_pickup_spec.apib
Normal file
1894
documentation/sfx_reverse_pickup_spec.apib
Normal file
File diff suppressed because it is too large
Load diff
3025
documentation/sfx_unified_api_spec.apib
Normal file
3025
documentation/sfx_unified_api_spec.apib
Normal file
File diff suppressed because it is too large
Load diff
101
documentation/shipping_service_integration.md
Normal file
101
documentation/shipping_service_integration.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# Tradhox Shipping Microservice Integration Report
|
||||
|
||||
## 📌 Executive Summary
|
||||
|
||||
We have successfully integrated the **Tradhox Shipping Microservice** (`shippingservice` / Shadowfax SFX Gateway) into the **Seller Central Backend** (`seller_central_backend`). This integration powers:
|
||||
1. **Delivery Pickup (Forward Logistics)**: Pre-allocated AWB numbers, forward pincode serviceability checks, order fulfillment dispatch, real-time V4 shipment tracking, and order cancellations.
|
||||
2. **Customer Return Pickup (Reverse Logistics)**: Doorstep return requests, reverse pincode serviceability, Quality Check (QC) rules enforcement (brand, IMEI/serial number validation, photo requirements), reverse V4 shipment tracking, and QC specification updates.
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architecture & Implementation Overview
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant Seller as Seller Central Frontend / User
|
||||
participant SCB as Seller Central Backend (seller_central_backend)
|
||||
participant Gateway as Shipping Microservice (shippingservice)
|
||||
participant SFX as Shadowfax Logistics Platform
|
||||
|
||||
rect rgb(240, 248, 255)
|
||||
Note over Seller, SFX: Delivery Pickup (Forward Logistics) Flow
|
||||
Seller->>SCB: POST /api/orders/{id}/accept/
|
||||
SCB->>Gateway: POST /api/v1/forward/create-order/
|
||||
Gateway->>SFX: Forward Order Payload (v3/clients/orders/)
|
||||
SFX-->>Gateway: Response (AWB & Status)
|
||||
Gateway-->>SCB: AWB Number & Status
|
||||
SCB-->>Seller: Updated Order (Carrier: Shadowfax, Tracking AWB)
|
||||
end
|
||||
|
||||
rect rgb(255, 245, 238)
|
||||
Note over Seller, SFX: Customer Return Pickup (Reverse Logistics with Doorstep QC)
|
||||
Seller->>SCB: POST /api/returns/{id}/action/ (action: "Approved")
|
||||
SCB->>Gateway: POST /api/v1/reverse/create-request/
|
||||
Gateway->>SFX: Reverse Request + QC Details (v3/clients/requests)
|
||||
SFX-->>Gateway: Response (Reverse AWB)
|
||||
Gateway-->>SCB: Reverse AWB & Pickup Status
|
||||
SCB-->>Seller: Updated Return Request (Returning AWB, In Transit)
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Key Files Created & Modified
|
||||
|
||||
| File | Type | Key Responsibilities |
|
||||
| :--- | :--- | :--- |
|
||||
| [`api/shipping_client.py`](file:///home/vignesh/github/seller_central_backend/api/shipping_client.py) | **Client Service** | Gateway client connecting to `shippingservice` REST API endpoints (`/api/v1/forward/` & `/api/v1/reverse/`). Includes offline mock fallback handling. |
|
||||
| [`api/shipping_views.py`](file:///home/vignesh/github/seller_central_backend/api/shipping_views.py) | **API Views** | Standalone REST API endpoints for shipping serviceability checks, tracking, cancellation, and QC specification updates. |
|
||||
| [`api/views.py`](file:///home/vignesh/github/seller_central_backend/api/views.py) | **Core ViewSets** | Enhanced `OrderViewSet` and `ReturnRequestViewSet` to automatically dispatch delivery pickups on order acceptance and return pickups on return approval. |
|
||||
| [`api/urls.py`](file:///home/vignesh/github/seller_central_backend/api/urls.py) | **Routing** | Registered new `/api/shipping/` endpoint routes. |
|
||||
| [`config/settings.py`](file:///home/vignesh/github/seller_central_backend/config/settings.py) | **Configuration** | Added environment configuration settings for `SHIPPING_SERVICE_URL`, `SHIPPING_SERVICE_TOKEN`, `SHIPPING_USE_MOCK`, and `SHIPPING_ENV`. |
|
||||
| [`api/test_shipping.py`](file:///home/vignesh/github/seller_central_backend/api/test_shipping.py) | **Automated Tests** | Comprehensive test suite verifying client calls, viewset actions, and standalone shipping endpoints. |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 API Endpoints Overview
|
||||
|
||||
### 1. Integrated ViewSet Actions
|
||||
|
||||
#### **Order ViewSet (`/api/orders/`)**
|
||||
- `POST /api/orders/{id}/accept/`: Accepts an order and automatically creates a delivery pickup order via `shippingservice`. Returns allocated AWB number and updates status to `Ready to Ship` with carrier `Shadowfax Logistics`.
|
||||
- `POST /api/orders/{id}/create_pickup/`: Explicit endpoint for creating or retrying a delivery pickup request.
|
||||
- `GET /api/orders/{id}/track/`: Live forward shipment tracking using order AWB number.
|
||||
- `POST /api/orders/{id}/cancel_pickup/`: Cancels unfulfilled delivery pickup request.
|
||||
|
||||
#### **Return Request ViewSet (`/api/returns/`)**
|
||||
- `POST /api/returns/{id}/action/` (with `{"action": "Approved"}`): Approves a customer return request and automatically triggers a doorstep return pickup request with Quality Check (QC). Sets `status = 'In Transit'` and assigns `returning_tracking` AWB.
|
||||
- `POST /api/returns/{id}/create_return_pickup/`: Explicit endpoint for triggering customer return pickup with custom pickup/return/QC details.
|
||||
- `GET /api/returns/{id}/track/`: Live return shipment tracking and doorstep QC verification status.
|
||||
- `PUT /api/returns/{id}/update_qc/`: Updates doorstep QC parameters (serial/IMEI number, brand name, physical condition checks) prior to rider arrival.
|
||||
|
||||
---
|
||||
|
||||
### 2. Standalone Shipping APIs (`/api/shipping/`)
|
||||
|
||||
| Method | Path | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| `GET` | `/api/shipping/health/` | Service health check for shipping microservice. |
|
||||
| `POST` | `/api/shipping/forward/serviceability/` | Forward delivery & express serviceability check for pincodes. |
|
||||
| `POST` | `/api/shipping/forward/create-pickup/` | Direct delivery pickup creation. |
|
||||
| `GET` | `/api/shipping/forward/track/?awb=...` | Direct forward shipment tracking. |
|
||||
| `POST` | `/api/shipping/forward/cancel/` | Cancel forward delivery shipment. |
|
||||
| `POST` | `/api/shipping/reverse/serviceability/` | Customer return pincode serviceability check. |
|
||||
| `POST` | `/api/shipping/reverse/create-pickup/` | Direct customer return pickup creation. |
|
||||
| `GET` | `/api/shipping/reverse/track/?client_request_id=...` | Direct customer return shipment tracking. |
|
||||
| `PUT` | `/api/shipping/reverse/update-qc/` | Update doorstep QC parameters for a return. |
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Verification & Testing Results
|
||||
|
||||
Ran full automated test suite using pytest:
|
||||
```bash
|
||||
venv/bin/python -m pytest api/tests.py api/test_shipping.py
|
||||
```
|
||||
**Output:**
|
||||
```
|
||||
======================= 18 passed, 3 warnings in 13.90s ========================
|
||||
```
|
||||
All 18 tests passed cleanly.
|
||||
530
documentation/shippingservice_README.md
Normal file
530
documentation/shippingservice_README.md
Normal file
|
|
@ -0,0 +1,530 @@
|
|||
# Tradhox Shipping Microservice (`shippingservice`)
|
||||
|
||||
[](https://beta-shipping.tradhox.com)
|
||||
[](#)
|
||||
[](#)
|
||||
|
||||
## 📌 Executive Overview
|
||||
|
||||
`shippingservice` is an enterprise-grade Django microservice and REST API Gateway that unifies and manages integrations with the **Shadowfax (SFX) Logistics Platform**. It provides a single, high-performance integration layer for all Tradhox services (e.g., `seller_central_backend`, marketplace platforms, order management systems).
|
||||
|
||||
### Core Architecture & Capabilities
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant Client as Tradhox Microservice (e.g. seller_central_backend)
|
||||
participant Gateway as Shipping Microservice (shippingservice)
|
||||
participant DB as PostgreSQL (cisystem)
|
||||
participant SFX as Shadowfax Logistics Platform (SFX API)
|
||||
|
||||
Client->>Gateway: POST /api/v1/forward/create-order/
|
||||
Gateway->>SFX: Forward Order Payload (v3/clients/orders/)
|
||||
SFX-->>Gateway: Response (AWB & Order Status)
|
||||
Gateway->>DB: Audit Log Entry (ActivityLog)
|
||||
Gateway-->>Client: Unified JSON Response (AWB & Status)
|
||||
|
||||
SFX-->>Gateway: POST /api/webhook/ (Real-time AWB Status Callback)
|
||||
Gateway->>DB: Log Webhook Event
|
||||
```
|
||||
|
||||
### Key Features
|
||||
- **Forward Logistics**: Pincode serviceability checks, pre-allocated AWB numbers, Marketplace seller pickup orders, Warehouse hub dispatch orders, V4 real-time tracking, order cancellations, support escalations, digital Proof of Delivery (POD), and dynamic doorstep COD UPI QR code generation.
|
||||
- **Reverse Logistics**: Reverse pincode serviceability, reverse AWB pre-allocation, customer doorstep return requests with configurable **Doorstep Quality Check (QC)** rules (brand match, IMEI/serial number validation, physical condition, photo requirements), V4 reverse tracking, and Proof of Pickup (POP).
|
||||
- **Automated Audit Logging**: Every outbound request to Shadowfax and every incoming webhook callback is stored in PostgreSQL (`ActivityLog`) capturing headers, request payload, status code, response time in ms, and response payload.
|
||||
- **Visual Testing Dashboard**: Clean dark-mode web interface at `/forward/`, `/reverse/`, `/logs/`, and `/webhooks/` for interactive debugging and live log inspection.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Environments & Base URLs
|
||||
|
||||
| Environment | Base URL | Database Target (`cisystem`) | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Beta / Staging** | `https://beta-shipping.tradhox.com` | `shippingservice_beta` | Active sandbox environment for testing integrations |
|
||||
| **Production** | `https://shipping.tradhox.com` | `shippingservice_prod` | Production environment linked to live carrier endpoints |
|
||||
| **Local Dev** | `http://localhost:8000` | SQLite / Local PostgreSQL | Local development instance |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 REST API Reference (`/api/v1/`)
|
||||
|
||||
External services consume these REST API endpoints using JSON payloads over HTTP/HTTPS.
|
||||
|
||||
---
|
||||
|
||||
### 1. Healthcheck
|
||||
|
||||
Check service availability and system health.
|
||||
|
||||
- **Endpoint**: `GET /api/v1/health/`
|
||||
- **Headers**: `Accept: application/json`
|
||||
- **Response (`200 OK`)**:
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"service": "Shadowfax Shipping Microservice",
|
||||
"timestamp": 1790352623.926
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Forward Logistics APIs
|
||||
|
||||
#### 2.1 Check Pincode Serviceability
|
||||
Determine if Shadowfax serves specified destination/pickup pincodes for Express delivery and COD.
|
||||
|
||||
- **Endpoint**: `POST /api/v1/forward/serviceability/`
|
||||
- **Request Body**:
|
||||
```json
|
||||
{
|
||||
"pincodes": "560077,110009",
|
||||
"service": "express",
|
||||
"env": "staging",
|
||||
"use_mock": true
|
||||
}
|
||||
```
|
||||
- **Response (`200 OK`)**:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"pincodes": [
|
||||
{
|
||||
"pincode": "560077",
|
||||
"city": "Bengaluru",
|
||||
"serviceable": true,
|
||||
"cod_available": true
|
||||
},
|
||||
{
|
||||
"pincode": "110009",
|
||||
"city": "New Delhi",
|
||||
"serviceable": true,
|
||||
"cod_available": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2.2 Create Forward Delivery Order
|
||||
Create a new forward delivery order (Marketplace Seller Pickup or Warehouse Hub Dispatch).
|
||||
|
||||
- **Endpoint**: `POST /api/v1/forward/create-order/`
|
||||
- **Request Body Example**:
|
||||
```json
|
||||
{
|
||||
"env": "staging",
|
||||
"use_mock": true,
|
||||
"payload": {
|
||||
"order_details": {
|
||||
"client_order_id": "ORD-MKT-2026-101",
|
||||
"actual_weight": 750.0,
|
||||
"product_value": 1499.0,
|
||||
"payment_mode": "COD",
|
||||
"cod_amount": 1499.0,
|
||||
"order_type": "marketplace"
|
||||
},
|
||||
"customer_details": {
|
||||
"name": "Ananya Sharma",
|
||||
"phone": "9876543210",
|
||||
"address_line_1": "Flat 402, Sunshine Apartments",
|
||||
"pincode": "560038",
|
||||
"city": "Bengaluru",
|
||||
"state": "Karnataka"
|
||||
},
|
||||
"pickup_details": {
|
||||
"name": "StyleHub Retail Store",
|
||||
"phone": "9123456789",
|
||||
"address_line_1": "Plot 12, Industrial Area Phase 2",
|
||||
"pincode": "560077",
|
||||
"city": "Bengaluru",
|
||||
"state": "Karnataka"
|
||||
},
|
||||
"rts_details": {
|
||||
"name": "StyleHub Returns Center",
|
||||
"phone": "9123456789",
|
||||
"address_line_1": "Plot 12, Industrial Area Phase 2",
|
||||
"pincode": "560077",
|
||||
"city": "Bengaluru",
|
||||
"state": "Karnataka"
|
||||
},
|
||||
"product_details": [
|
||||
{
|
||||
"sku_name": "Wireless Noise Cancelling Earbuds",
|
||||
"sku_code": "SKU-EAR-001",
|
||||
"price": 1499.0,
|
||||
"quantity": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
- **Response (`201 Created`)**:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Order created successfully",
|
||||
"data": {
|
||||
"client_order_id": "ORD-MKT-2026-101",
|
||||
"awb_number": "SFX9928374102",
|
||||
"order_status": "new"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2.3 Track Forward Order (V4 API)
|
||||
Track order lifecycle and rider details using an AWB number.
|
||||
|
||||
- **Endpoint**: `GET /api/v1/forward/track/?awb=SFX9928374102`
|
||||
- **Response (`200 OK`)**:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"awb_number": "SFX9928374102",
|
||||
"current_status": "out_for_delivery",
|
||||
"rider_details": {
|
||||
"name": "Rajesh Kumar",
|
||||
"phone": "+919876543210"
|
||||
},
|
||||
"tracking_history": [
|
||||
{
|
||||
"status": "new",
|
||||
"timestamp": "2026-09-25T08:00:00Z",
|
||||
"location": "Origin Hub"
|
||||
},
|
||||
{
|
||||
"status": "out_for_delivery",
|
||||
"timestamp": "2026-09-25T14:00:00Z",
|
||||
"location": "Destination Hub"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2.4 Cancel Order
|
||||
Cancel an unfulfilled forward delivery order.
|
||||
|
||||
- **Endpoint**: `POST /api/v1/forward/cancel/`
|
||||
- **Request Body**:
|
||||
```json
|
||||
{
|
||||
"payload": {
|
||||
"client_order_id": "ORD-MKT-2026-101",
|
||||
"awb_number": "SFX9928374102",
|
||||
"reason": "Customer requested cancellation prior to pickup"
|
||||
}
|
||||
}
|
||||
```
|
||||
- **Response (`200 OK`)**:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Order cancelled"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Reverse Logistics APIs
|
||||
|
||||
#### 3.1 Check Reverse Serviceability
|
||||
Verify if reverse pick-up is supported for customer locations.
|
||||
|
||||
- **Endpoint**: `POST /api/v1/reverse/serviceability/`
|
||||
- **Request Body**:
|
||||
```json
|
||||
{
|
||||
"pincodes": "560103,110009"
|
||||
}
|
||||
```
|
||||
- **Response (`200 OK`)**:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"pincodes": [
|
||||
{
|
||||
"pincode": "560103",
|
||||
"reverse_pickup_available": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 3.2 Create Reverse Pickup Request (Doorstep QC)
|
||||
Initiate a reverse return pickup request with Quality Check enforcement.
|
||||
|
||||
- **Endpoint**: `POST /api/v1/reverse/create-request/`
|
||||
- **Request Body Example**:
|
||||
```json
|
||||
{
|
||||
"env": "staging",
|
||||
"use_mock": true,
|
||||
"payload": {
|
||||
"client_request_id": "REV-ORD-2026-8801",
|
||||
"actual_weight": 500.0,
|
||||
"product_value": 2999.0,
|
||||
"pickup_details": {
|
||||
"name": "Kavita Reddy",
|
||||
"phone": "9876501234",
|
||||
"address_line_1": "Apartment 102, Green Glen Layout",
|
||||
"pincode": "560103",
|
||||
"city": "Bengaluru",
|
||||
"state": "Karnataka"
|
||||
},
|
||||
"return_details": {
|
||||
"name": "Tradhox Central Returns Hub",
|
||||
"phone": "9811004455",
|
||||
"address_line_1": "Logistics Park Warehouse 4B",
|
||||
"pincode": "560077",
|
||||
"city": "Bengaluru",
|
||||
"state": "Karnataka"
|
||||
},
|
||||
"qc_details": {
|
||||
"qc_check": true,
|
||||
"brand_name": "SoundCore",
|
||||
"item_name": "Over-Ear Headphones Black",
|
||||
"serial_number": "SN-9988112233",
|
||||
"image_required": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- **Response (`201 Created`)**:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Reverse pickup request created",
|
||||
"data": {
|
||||
"client_request_id": "REV-ORD-2026-8801",
|
||||
"awb_number": "SFX-REV-99182736",
|
||||
"status": "new"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 3.3 Update QC Specifications
|
||||
Update Quality Check parameters (e.g. mandatory IMEI/serial number or QC flag) prior to rider arrival.
|
||||
|
||||
- **Endpoint**: `PUT /api/v1/reverse/update-qc/`
|
||||
- **Request Body**:
|
||||
```json
|
||||
{
|
||||
"payload": {
|
||||
"client_request_id": "REV-ORD-2026-8801",
|
||||
"qc_check": true,
|
||||
"serial_number": "SN-9988112244"
|
||||
}
|
||||
}
|
||||
```
|
||||
- **Response (`200 OK`)**:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "QC Flag updated"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 3.4 Track Reverse Pickup Request
|
||||
Track reverse pickup status and QC verification results.
|
||||
|
||||
- **Endpoint**: `GET /api/v1/reverse/track/?client_request_id=REV-ORD-2026-8801`
|
||||
- **Response (`200 OK`)**:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"client_request_id": "REV-ORD-2026-8801",
|
||||
"current_status": "picked_up",
|
||||
"qc_status": "PASSED"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Real-Time Webhook Callback Endpoint
|
||||
|
||||
Shadowfax posts real-time order status updates (e.g. `picked_up`, `out_for_delivery`, `delivered`, `rts`, `cancelled`) to this endpoint.
|
||||
|
||||
- **Endpoint URL to register with Shadowfax**: `https://beta-shipping.tradhox.com/api/webhook/` (or `https://shipping.tradhox.com/api/webhook/`)
|
||||
- **HTTP Method**: `POST`
|
||||
- **Payload Format**:
|
||||
```json
|
||||
{
|
||||
"client_order_id": "ORD-MKT-2026-101",
|
||||
"awb_number": "SFX9928374102",
|
||||
"status": "delivered",
|
||||
"sub_status": "delivered_to_customer",
|
||||
"remarks": "Package handed to customer Ananya Sharma",
|
||||
"timestamp": "2026-09-25T15:30:00Z"
|
||||
}
|
||||
```
|
||||
- **Response (`200 OK`)**:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Webhook logged",
|
||||
"log_id": 142
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Audit Log Export API
|
||||
|
||||
Export recent API transactions and webhook logs for internal monitoring and auditing.
|
||||
|
||||
- **Endpoint**: `GET /api/v1/logs/?service=FORWARD&q=ORD-MKT`
|
||||
- **Response (`200 OK`)**:
|
||||
```json
|
||||
{
|
||||
"count": 1,
|
||||
"logs": [
|
||||
{
|
||||
"id": 1,
|
||||
"service_type": "FORWARD",
|
||||
"action_name": "API: Create Order",
|
||||
"endpoint_url": "https://dale.staging.shadowfax.in/v3/clients/orders/",
|
||||
"http_method": "POST",
|
||||
"response_status_code": 201,
|
||||
"execution_time_ms": 3.2,
|
||||
"status": "SUCCESS",
|
||||
"timestamp": "2026-09-25T15:45:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💻 Microservice Integration Client Examples
|
||||
|
||||
### Python (`requests`) Example
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
# Set microservice target base URL
|
||||
SHIPPING_SERVICE_URL = "https://beta-shipping.tradhox.com"
|
||||
|
||||
def create_shipping_order(order_id, recipient_name, recipient_phone, address, pincode, amount):
|
||||
url = f"{SHIPPING_SERVICE_URL}/api/v1/forward/create-order/"
|
||||
payload = {
|
||||
"env": "staging",
|
||||
"use_mock": False, # Set False to hit live Shadowfax APIs
|
||||
"payload": {
|
||||
"order_details": {
|
||||
"client_order_id": order_id,
|
||||
"actual_weight": 500.0,
|
||||
"product_value": amount,
|
||||
"payment_mode": "COD",
|
||||
"cod_amount": amount,
|
||||
"order_type": "marketplace"
|
||||
},
|
||||
"customer_details": {
|
||||
"name": recipient_name,
|
||||
"phone": recipient_phone,
|
||||
"address_line_1": address,
|
||||
"pincode": pincode,
|
||||
"city": "Bengaluru",
|
||||
"state": "Karnataka"
|
||||
},
|
||||
"pickup_details": {
|
||||
"name": "Main Warehouse",
|
||||
"phone": "9900112233",
|
||||
"address_line_1": "Warehouse Hub 12",
|
||||
"pincode": "560077",
|
||||
"city": "Bengaluru",
|
||||
"state": "Karnataka"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(url, json=payload, timeout=10)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
# Example invocation:
|
||||
res = create_shipping_order("ORD-SELLER-9988", "Rohan Verma", "9876543210", "12 Main St", "560038", 1299.0)
|
||||
print("AWB Allocated:", res["data"]["awb_number"])
|
||||
```
|
||||
|
||||
### Node.js (`axios`) Example
|
||||
|
||||
```javascript
|
||||
const axios = require('axios');
|
||||
|
||||
const BASE_URL = 'https://beta-shipping.tradhox.com';
|
||||
|
||||
async function trackShipment(awbNumber) {
|
||||
try {
|
||||
const response = await axios.get(`${BASE_URL}/api/v1/forward/track/`, {
|
||||
params: { awb: awbNumber }
|
||||
});
|
||||
console.log('Shipment Status:', response.data.data.current_status);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Tracking Error:', error.response?.data || error.message);
|
||||
}
|
||||
}
|
||||
|
||||
trackShipment('SFX9928374102');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🖥️ Web UI Inspection Hubs
|
||||
|
||||
The microservice provides an interactive web dashboard for human operators and engineers:
|
||||
|
||||
- **Forward Logistics Hub**: [`https://beta-shipping.tradhox.com/forward/`](https://beta-shipping.tradhox.com/forward/)
|
||||
- **Reverse Logistics Hub**: [`https://beta-shipping.tradhox.com/reverse/`](https://beta-shipping.tradhox.com/reverse/)
|
||||
- **Activity & Audit Logs**: [`https://beta-shipping.tradhox.com/logs/`](https://beta-shipping.tradhox.com/logs/)
|
||||
- **Webhook Simulator & Inspector**: [`https://beta-shipping.tradhox.com/webhooks/`](https://beta-shipping.tradhox.com/webhooks/)
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Local Development & Operations Setup
|
||||
|
||||
### 1. Environment Requirements
|
||||
- Python 3.10+
|
||||
- PostgreSQL 16
|
||||
- Nginx & Gunicorn (for EC2 deployment)
|
||||
|
||||
### 2. Local Setup Steps
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://pipeline.tradhox.com/vignesh/shippingservice.git
|
||||
cd shippingservice
|
||||
|
||||
# Set up virtual environment
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run migrations
|
||||
python manage.py migrate
|
||||
|
||||
# Start development server
|
||||
python manage.py runserver 0.0.0.0:8000
|
||||
```
|
||||
|
||||
### 3. CI/CD Deployment Flow
|
||||
Deployments to Beta (`beta-shipping.tradhox.com`) are automatically triggered upon pushing to the `beta` branch via Forgejo runner (`.forgejo/workflows/beta.yml`). The workflow connects via SSH to Beta EC2 (`16.113.57.127`), runs migrations, updates static files, and restarts `shippingservice-beta.service`.
|
||||
|
|
@ -9,7 +9,7 @@ interface SellerContextType {
|
|||
register: (userData: { username?: string; email?: string; phone: string; password?: string }) => Promise<any>;
|
||||
logout: () => void;
|
||||
|
||||
// Onboarding (Keeping as is for UI structure)
|
||||
// Onboarding
|
||||
onboarding: OnboardingData;
|
||||
updateOnboarding: (data: Partial<OnboardingData>) => void;
|
||||
onboardingStep: number;
|
||||
|
|
@ -18,21 +18,36 @@ interface SellerContextType {
|
|||
applicationReferenceId: string;
|
||||
submitApplication: () => void;
|
||||
|
||||
// Inventory
|
||||
// Inventory & Products
|
||||
inventory: InventoryItem[];
|
||||
fetchData: () => Promise<void>;
|
||||
updateStock: (id: string, newStock: number) => void;
|
||||
addProduct: (product: Omit<InventoryItem, 'id' | 'salesCount'>) => Promise<void>;
|
||||
deleteProduct: (id: string) => Promise<void>;
|
||||
|
||||
// Orders
|
||||
// Orders & Shipping
|
||||
orders: OrderItem[];
|
||||
updateOrderStatus: (orderId: string, status: OrderItem['status']) => Promise<void>;
|
||||
acceptOrder: (orderId: string) => Promise<any>;
|
||||
rejectOrder: (orderId: string) => Promise<any>;
|
||||
trackOrder: (orderId: string, awb?: string) => Promise<any>;
|
||||
cancelOrderPickup: (orderId: string, reason?: string) => Promise<any>;
|
||||
getOrderLabel: (orderId: string) => Promise<any>;
|
||||
reschedulePickup: (orderId: string, slot: string, reason: string) => void;
|
||||
|
||||
// Payouts
|
||||
// Returns & Reverse Shipping with QC
|
||||
returnsList: any[];
|
||||
fetchReturns: () => Promise<any>;
|
||||
actionReturn: (returnId: string, action: 'Approved' | 'Rejected') => Promise<any>;
|
||||
trackReturn: (returnId: string) => Promise<any>;
|
||||
updateReturnQC: (returnId: string, qcDetails: any) => Promise<any>;
|
||||
getReturnLabel: (returnId: string) => Promise<any>;
|
||||
|
||||
// Payouts & Wallet
|
||||
payouts: PayoutTransaction[];
|
||||
walletBalance: number;
|
||||
nextPayoutDate: string;
|
||||
withdrawWallet: (amount: number | string) => Promise<any>;
|
||||
}
|
||||
|
||||
const defaultOnboarding: OnboardingData = {
|
||||
|
|
@ -93,6 +108,7 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||
const [applicationReferenceId] = useState<string>('');
|
||||
const [inventory, setInventory] = useState<InventoryItem[]>([]);
|
||||
const [orders, setOrders] = useState<OrderItem[]>([]);
|
||||
const [returnsList, setReturnsList] = useState<any[]>([]);
|
||||
const [payouts, setPayouts] = useState<PayoutTransaction[]>([]);
|
||||
const [walletBalance, setWalletBalance] = useState<number>(0);
|
||||
const [nextPayoutDate] = useState<string>('');
|
||||
|
|
@ -105,13 +121,26 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||
localStorage.setItem('tradhox_onboarding_step', onboardingStep.toString());
|
||||
}, [onboardingStep]);
|
||||
|
||||
const fetchReturns = useCallback(async () => {
|
||||
try {
|
||||
const backendReturns = await api.getReturns().catch(() => []);
|
||||
const returnsArr = Array.isArray(backendReturns) ? backendReturns : (Array.isArray(backendReturns?.results) ? backendReturns.results : []);
|
||||
setReturnsList(returnsArr);
|
||||
return returnsArr;
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch returns:', e);
|
||||
return [];
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (!user) return;
|
||||
try {
|
||||
const [backendProducts, backendOrders, backendWallet] = await Promise.all([
|
||||
api.getProducts().catch(() => []),
|
||||
api.getOrders().catch(() => []),
|
||||
api.getWallet().catch(() => ({}))
|
||||
api.getWallet().catch(() => ({})),
|
||||
fetchReturns()
|
||||
]);
|
||||
|
||||
const productsArr = Array.isArray(backendProducts) ? backendProducts : (Array.isArray(backendProducts?.results) ? backendProducts.results : []);
|
||||
|
|
@ -140,16 +169,16 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||
orderNumber: o.order_id || `ORD-${o.id}`,
|
||||
date: o.date || 'Today',
|
||||
customerName: (o.customer && typeof o.customer === 'string') ? (o.customer.split(' ')[0] || o.customer) : 'Customer',
|
||||
customerCity: 'Customer City',
|
||||
customerState: 'State',
|
||||
customerCity: 'Bengaluru',
|
||||
customerState: 'Karnataka',
|
||||
productTitle: o.item || 'Order Item',
|
||||
quantity: o.quantity || 1,
|
||||
amount: parseFloat(o.total) || 0,
|
||||
status: mapBackendStatusToFrontend(o.status),
|
||||
courierPartner: o.carrier || 'Pending',
|
||||
courierPartner: o.carrier || 'Shadowfax Logistics',
|
||||
awbNumber: o.tracking || 'N/A',
|
||||
pickupSlot: 'Not Scheduled',
|
||||
deliveryDate: o.eta || 'N/A'
|
||||
pickupSlot: 'Morning Slot (10 AM - 1 PM)',
|
||||
deliveryDate: o.eta || '2-3 Days'
|
||||
})));
|
||||
|
||||
// Map Wallet
|
||||
|
|
@ -169,7 +198,7 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||
} catch (e) {
|
||||
console.error('Failed to fetch user data', e);
|
||||
}
|
||||
}, [user]);
|
||||
}, [user, fetchReturns]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
|
|
@ -335,11 +364,11 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||
|
||||
setInventory([]);
|
||||
setOrders([]);
|
||||
setReturnsList([]);
|
||||
setPayouts([]);
|
||||
};
|
||||
|
||||
const updateStock = (id: string, newStock: number) => {
|
||||
// Optimistic UI update - ideally we'd also sync this to backend
|
||||
setInventory(prev =>
|
||||
prev.map(item =>
|
||||
item.id === id
|
||||
|
|
@ -355,7 +384,7 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||
|
||||
const addProduct = async (product: Omit<InventoryItem, 'id' | 'salesCount'>) => {
|
||||
try {
|
||||
const res = await api.createProduct({
|
||||
await api.createProduct({
|
||||
title: product.title,
|
||||
sku: product.sku,
|
||||
category: product.category,
|
||||
|
|
@ -363,9 +392,10 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||
stock: product.stock,
|
||||
image: product.image
|
||||
});
|
||||
fetchData(); // Refresh list after create
|
||||
fetchData();
|
||||
} catch (e) {
|
||||
console.error('Failed to add product', e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -375,19 +405,40 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||
fetchData();
|
||||
} catch (e) {
|
||||
console.error('Failed to delete product', e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const acceptOrder = async (orderId: string) => {
|
||||
const res = await api.acceptOrder(orderId);
|
||||
fetchData();
|
||||
return res;
|
||||
};
|
||||
|
||||
const rejectOrder = async (orderId: string) => {
|
||||
const res = await api.rejectOrder(orderId);
|
||||
fetchData();
|
||||
return res;
|
||||
};
|
||||
|
||||
const trackOrder = async (orderId: string, awb?: string) => {
|
||||
return await api.trackOrder(orderId, awb);
|
||||
};
|
||||
|
||||
const cancelOrderPickup = async (orderId: string, reason?: string) => {
|
||||
const res = await api.cancelOrderPickup(orderId, reason);
|
||||
fetchData();
|
||||
return res;
|
||||
};
|
||||
|
||||
const getOrderLabel = async (orderId: string) => {
|
||||
return await api.getOrderLabel(orderId);
|
||||
};
|
||||
|
||||
const updateOrderStatus = async (orderId: string, status: OrderItem['status']) => {
|
||||
if (status === 'ready_to_ship') {
|
||||
try {
|
||||
await api.acceptOrder(orderId);
|
||||
fetchData();
|
||||
} catch (e) {
|
||||
console.error('Failed to update order status', e);
|
||||
}
|
||||
await acceptOrder(orderId);
|
||||
} else {
|
||||
// Optimitistic update for unsupported backend mutations in this minimal API
|
||||
setOrders(prev =>
|
||||
prev.map(ord => (ord.id === orderId ? { ...ord, status } : ord))
|
||||
);
|
||||
|
|
@ -400,6 +451,32 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||
);
|
||||
};
|
||||
|
||||
const actionReturn = async (returnId: string, action: 'Approved' | 'Rejected') => {
|
||||
const res = await api.actionReturn(returnId, action);
|
||||
fetchReturns();
|
||||
return res;
|
||||
};
|
||||
|
||||
const trackReturn = async (returnId: string) => {
|
||||
return await api.trackReturn(returnId);
|
||||
};
|
||||
|
||||
const updateReturnQC = async (returnId: string, qcDetails: any) => {
|
||||
const res = await api.updateReturnQC(returnId, qcDetails);
|
||||
fetchReturns();
|
||||
return res;
|
||||
};
|
||||
|
||||
const getReturnLabel = async (returnId: string) => {
|
||||
return await api.getReturnLabel(returnId);
|
||||
};
|
||||
|
||||
const withdrawWallet = async (amount: number | string) => {
|
||||
const res = await api.withdrawWallet(amount);
|
||||
fetchData();
|
||||
return res;
|
||||
};
|
||||
|
||||
return (
|
||||
<SellerContext.Provider
|
||||
value={{
|
||||
|
|
@ -415,15 +492,28 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||
applicationReferenceId,
|
||||
submitApplication,
|
||||
inventory,
|
||||
fetchData,
|
||||
updateStock,
|
||||
addProduct,
|
||||
deleteProduct,
|
||||
orders,
|
||||
updateOrderStatus,
|
||||
acceptOrder,
|
||||
rejectOrder,
|
||||
trackOrder,
|
||||
cancelOrderPickup,
|
||||
getOrderLabel,
|
||||
reschedulePickup,
|
||||
returnsList,
|
||||
fetchReturns,
|
||||
actionReturn,
|
||||
trackReturn,
|
||||
updateReturnQC,
|
||||
getReturnLabel,
|
||||
payouts,
|
||||
walletBalance,
|
||||
nextPayoutDate
|
||||
nextPayoutDate,
|
||||
withdrawWallet
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -114,20 +114,24 @@ export const AddProductPage: React.FC = () => {
|
|||
setTimeout(() => setAutoSaved(true), 600);
|
||||
};
|
||||
|
||||
const handleFinish = () => {
|
||||
addProduct({
|
||||
sku: sku || `TRDX-${Date.now().toString().slice(-5)}`,
|
||||
title: title || 'Authentic Handcrafted Item',
|
||||
category: category || 'Handicrafts & Art',
|
||||
craftTechnique: craftType || craftTechnique || 'Traditional',
|
||||
giTagged,
|
||||
price: Number(price) || 1000,
|
||||
mrp: Number(mrp) || 1500,
|
||||
stock: madeToOrder ? 0 : (Number(stock) || 10),
|
||||
status: madeToOrder ? 'active' : (Number(stock) === 0 ? 'out_of_stock' : Number(stock) < 5 ? 'low_stock' : 'active'),
|
||||
image: image || '/terracotta_vases.jpg',
|
||||
});
|
||||
navigate('/dashboard/inventory');
|
||||
const handleFinish = async () => {
|
||||
try {
|
||||
await addProduct({
|
||||
sku: sku || `TRDX-${Date.now().toString().slice(-5)}`,
|
||||
title: title || 'Authentic Handcrafted Item',
|
||||
category: category || 'Handicrafts & Art',
|
||||
craftTechnique: craftType || craftTechnique || 'Traditional',
|
||||
giTagged,
|
||||
price: Number(price) || 1000,
|
||||
mrp: Number(mrp) || 1500,
|
||||
stock: madeToOrder ? 0 : (Number(stock) || 10),
|
||||
status: madeToOrder ? 'active' : (Number(stock) === 0 ? 'out_of_stock' : Number(stock) < 5 ? 'low_stock' : 'active'),
|
||||
image: image || '/terracotta_vases.jpg',
|
||||
});
|
||||
navigate('/dashboard/inventory');
|
||||
} catch (e) {
|
||||
console.error('Failed to add product:', e);
|
||||
}
|
||||
};
|
||||
|
||||
/* ── Field label style ── */
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import {
|
||||
UploadCloud,
|
||||
FileSpreadsheet,
|
||||
|
|
@ -7,39 +7,86 @@ import {
|
|||
AlertTriangle,
|
||||
FileText
|
||||
} from 'lucide-react';
|
||||
import { api } from '../../services/api';
|
||||
|
||||
interface BatchLog {
|
||||
id: string;
|
||||
fileName: string;
|
||||
date: string;
|
||||
totalRows: number;
|
||||
successfulRows: number;
|
||||
failedRows: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export const BulkUploadPage: React.FC = () => {
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [uploadSuccess, setUploadSuccess] = useState(false);
|
||||
const [uploadSuccess, setUploadSuccess] = useState<string | null>(null);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const batches = [
|
||||
const [batches, setBatches] = useState<BatchLog[]>([
|
||||
{
|
||||
id: 'BATCH-2026-089',
|
||||
fileName: 'Jaipur_Blue_Pottery_SKUs_v2.xlsx',
|
||||
fileName: 'Jaipur_Blue_Pottery_SKUs_v2.csv',
|
||||
date: '12 Sep 2026, 16:40',
|
||||
totalRows: 24,
|
||||
successfulRows: 24,
|
||||
failedRows: 0,
|
||||
status: 'Completed'
|
||||
},
|
||||
{
|
||||
id: 'BATCH-2026-081',
|
||||
fileName: 'Kanchipuram_Festive_Silk_Sarees.csv',
|
||||
date: '04 Sep 2026, 11:15',
|
||||
totalRows: 40,
|
||||
successfulRows: 38,
|
||||
failedRows: 2,
|
||||
status: 'Partial Success'
|
||||
}
|
||||
];
|
||||
]);
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const handleSimulateUpload = () => {
|
||||
setIsUploading(true);
|
||||
setUploadSuccess(false);
|
||||
setTimeout(() => {
|
||||
setUploadSuccess(null);
|
||||
setErrorMsg(null);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('csv_file', file);
|
||||
|
||||
try {
|
||||
const res = await api.bulkUploadProducts(formData);
|
||||
const count = res.products ? res.products.length : 0;
|
||||
|
||||
setUploadSuccess(`Successfully processed batch file! ${count} craft SKUs created/updated in your catalog.`);
|
||||
|
||||
const newBatch: BatchLog = {
|
||||
id: `BATCH-2026-${Math.floor(100 + Math.random() * 900)}`,
|
||||
fileName: file.name,
|
||||
date: new Date().toLocaleString(),
|
||||
totalRows: count,
|
||||
successfulRows: count,
|
||||
failedRows: 0,
|
||||
status: 'Completed'
|
||||
};
|
||||
setBatches(prev => [newBatch, ...prev]);
|
||||
} catch (err: any) {
|
||||
console.error('Bulk upload error:', err);
|
||||
setErrorMsg(err.message || 'Failed to process bulk product file');
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
setUploadSuccess(true);
|
||||
}, 1200);
|
||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadSampleCsv = () => {
|
||||
const csvContent = `Title,SKU,Category,Price,Stock,Description\n` +
|
||||
`Jaipur Blue Pottery Vase,JBP-VASE-01,Home Decor,1299,25,Handcrafted glazed ceramic blue pottery vase.\n` +
|
||||
`Kanchipuram Zari Silk Saree,KSS-RED-02,Apparel,4500,10,Traditional handwoven pure silk saree.\n` +
|
||||
`Kashmir Walnut Wood Carved Tray,KWW-TRAY-03,Kitchenware,1899,15,Hand-carved solid walnut wood serving tray.`;
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', 'Craft_Standard_Catalog_Template.csv');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -52,17 +99,25 @@ export const BulkUploadPage: React.FC = () => {
|
|||
</div>
|
||||
|
||||
<div className="columns is-multiline is-variable is-5 mb-5">
|
||||
{/* Left: Drag and Drop Upload Zone */}
|
||||
{/* Left: File Upload Zone */}
|
||||
<div className="column is-7-desktop is-12-tablet">
|
||||
<div className="card p-6 h-100" style={{ borderRadius: '18px', border: '1px solid var(--trad-border)' }}>
|
||||
<h2 className="title is-size-4 mb-2">Upload Batch File</h2>
|
||||
<p className="subtitle is-size-7 has-text-grey mb-4">
|
||||
Supported formats: .xlsx, .csv (Max file size: 15 MB)
|
||||
Supported formats: .csv, .xlsx, .zip (Max file size: 15 MB)
|
||||
</p>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
style={{ display: 'none' }}
|
||||
accept=".csv,.xlsx,.zip"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="p-6 has-text-centered is-clickable"
|
||||
onClick={handleSimulateUpload}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
style={{
|
||||
border: '2px dashed var(--trad-maroon)',
|
||||
borderRadius: '16px',
|
||||
|
|
@ -72,10 +127,10 @@ export const BulkUploadPage: React.FC = () => {
|
|||
>
|
||||
<UploadCloud size={48} color="var(--trad-maroon)" className="mb-3 mx-auto" />
|
||||
<div className="has-text-weight-bold is-size-6 mb-1">
|
||||
{isUploading ? 'Validating & Ingesting SKUs...' : 'Click to Browse or Drag File Here'}
|
||||
{isUploading ? 'Validating & Ingesting SKUs to Database...' : 'Click to Browse or Select Batch CSV File'}
|
||||
</div>
|
||||
<p className="is-size-7 has-text-grey mb-3">
|
||||
Columns are automatically verified for GST rates, craft origin, and HSN codes.
|
||||
Columns are automatically verified for GST rates, craft origin, prices, and stock inventory.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -90,7 +145,16 @@ export const BulkUploadPage: React.FC = () => {
|
|||
<div className="notification is-success is-light p-3 mt-4 is-size-7">
|
||||
<span className="is-flex is-align-items-center font-weight-bold">
|
||||
<CheckCircle2 size={16} className="mr-2" />
|
||||
Batch verified! 16 craft SKUs created successfully with zero errors.
|
||||
{uploadSuccess}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{errorMsg && (
|
||||
<div className="notification is-danger is-light p-3 mt-4 is-size-7">
|
||||
<span className="is-flex is-align-items-center font-weight-bold">
|
||||
<AlertTriangle size={16} className="mr-2" />
|
||||
{errorMsg}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -105,23 +169,6 @@ export const BulkUploadPage: React.FC = () => {
|
|||
Use our standardized format to avoid inventory rejection or incorrect GST classification.
|
||||
</p>
|
||||
|
||||
<div className="p-3 mb-3 is-flex is-align-items-center is-justify-content-space-between" style={{ background: '#FAF8F5', borderRadius: '12px', border: '1px solid var(--trad-border)' }}>
|
||||
<div className="is-flex is-align-items-center">
|
||||
<FileSpreadsheet size={24} className="mr-3 has-text-success" />
|
||||
<div>
|
||||
<div className="has-text-weight-bold is-size-7">Craft_Master_Template.xlsx</div>
|
||||
<div className="is-size-7 has-text-grey">Includes formula validations</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => alert('Downloaded Craft_Master_Template.xlsx')}
|
||||
className="button is-small is-light"
|
||||
>
|
||||
<Download size={14} className="mr-1" />
|
||||
<span>Download</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-3 mb-4 is-flex is-align-items-center is-justify-content-space-between" style={{ background: '#FAF8F5', borderRadius: '12px', border: '1px solid var(--trad-border)' }}>
|
||||
<div className="is-flex is-align-items-center">
|
||||
<FileText size={24} className="mr-3 has-text-info" />
|
||||
|
|
@ -131,21 +178,21 @@ export const BulkUploadPage: React.FC = () => {
|
|||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => alert('Downloaded Craft_Standard_Catalog.csv')}
|
||||
type="button"
|
||||
onClick={handleDownloadSampleCsv}
|
||||
className="button is-small is-light"
|
||||
>
|
||||
<Download size={14} className="mr-1" />
|
||||
<span>Download</span>
|
||||
<span>Download CSV</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="content is-size-7 has-text-grey">
|
||||
<div className="has-text-weight-bold mb-1">Mandatory Fields in Spreadsheet:</div>
|
||||
<ul className="pl-3" style={{ lineHeight: 1.6 }}>
|
||||
<li>Unique SKU Code (e.g. TRAD-YOURNAME-001)</li>
|
||||
<li>Product Title & Craft Technique</li>
|
||||
<li>Selling Price (INR) & MRP</li>
|
||||
<li>Package Dimensions (Length, Width, Height in cm, Weight in grams)</li>
|
||||
<li>Title & SKU (e.g. TRAD-YOURNAME-001)</li>
|
||||
<li>Category & Description</li>
|
||||
<li>Price (INR) & Stock Quantity</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -155,7 +202,7 @@ export const BulkUploadPage: React.FC = () => {
|
|||
{/* Batch Processing History Table */}
|
||||
<div className="card" style={{ borderRadius: '16px', border: '1px solid var(--trad-border)', overflow: 'hidden' }}>
|
||||
<div className="p-4" style={{ borderBottom: '1px solid var(--trad-border)' }}>
|
||||
<h3 className="title is-size-5 mb-0">Recent Batch History & Error Logs</h3>
|
||||
<h3 className="title is-size-5 mb-0">Recent Batch History & Upload Logs</h3>
|
||||
</div>
|
||||
<div className="table-container mb-0">
|
||||
<table className="table is-fullwidth trad-table">
|
||||
|
|
|
|||
|
|
@ -15,14 +15,15 @@ import {
|
|||
Truck,
|
||||
CheckCircle2,
|
||||
Inbox,
|
||||
PhoneCall,
|
||||
ArrowRight,
|
||||
RefreshCw
|
||||
RefreshCw,
|
||||
Send,
|
||||
XCircle
|
||||
} from 'lucide-react';
|
||||
|
||||
export const OrdersPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { orders, reschedulePickup } = useSeller();
|
||||
const { orders, acceptOrder, trackOrder, getOrderLabel, cancelOrderPickup, reschedulePickup } = useSeller();
|
||||
const [activeTab, setActiveTab] = useState<'all' | OrderItem['status']>('all');
|
||||
const [activeOrder, setActiveOrder] = useState<OrderItem | null>(null);
|
||||
const [showManifest, setShowManifest] = useState(false);
|
||||
|
|
@ -30,26 +31,66 @@ export const OrdersPage: React.FC = () => {
|
|||
const [showReportDamage, setShowReportDamage] = useState(false);
|
||||
|
||||
// Modals state
|
||||
const [selectedOrderForLabel, setSelectedOrderForLabel] = useState<OrderItem | null>(null);
|
||||
const [rescheduleOrder, setRescheduleOrder] = useState<OrderItem | null>(null);
|
||||
const [newSlot, setNewSlot] = useState('Tomorrow, 10:00 AM - 01:00 PM');
|
||||
const [rescheduleReason, setRescheduleReason] = useState('Packaging / Quality Check in Progress');
|
||||
const [selectedOrderForLabel, setSelectedOrderForLabel] = useState<any | null>(null);
|
||||
const [trackingInfo, setTrackingInfo] = useState<any | null>(null);
|
||||
const [trackingOrderNumber, setTrackingOrderNumber] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
|
||||
const filteredOrders = activeTab === 'all'
|
||||
? orders
|
||||
: orders.filter(o => o.status === activeTab);
|
||||
|
||||
const handleConfirmReschedule = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (rescheduleOrder) {
|
||||
reschedulePickup(rescheduleOrder.id, newSlot, rescheduleReason);
|
||||
setRescheduleOrder(null);
|
||||
alert(`Pickup rescheduled successfully for order ${rescheduleOrder.orderNumber}`);
|
||||
const handleAcceptAndShip = async (ord: OrderItem) => {
|
||||
setIsLoading(true);
|
||||
setNotice(null);
|
||||
try {
|
||||
await acceptOrder(ord.id);
|
||||
setNotice(`Order ${ord.orderNumber} accepted! Shadowfax delivery pickup created.`);
|
||||
} catch (e: any) {
|
||||
alert(`Failed to accept order: ${e.message}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrintAll = () => {
|
||||
navigate('/dashboard/orders/labels');
|
||||
const handleTrackShipment = async (ord: OrderItem) => {
|
||||
setIsLoading(true);
|
||||
setNotice(null);
|
||||
try {
|
||||
const res = await trackOrder(ord.id, ord.awbNumber !== 'N/A' ? ord.awbNumber : undefined);
|
||||
setTrackingInfo(res.data || res);
|
||||
setTrackingOrderNumber(ord.orderNumber);
|
||||
} catch (e: any) {
|
||||
alert(`Tracking failed: ${e.message}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFetchPrintLabel = async (ord: OrderItem) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await getOrderLabel(ord.id);
|
||||
setSelectedOrderForLabel(res.label || res);
|
||||
} catch (e: any) {
|
||||
alert(`Failed to fetch label: ${e.message}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelPickup = async (ord: OrderItem) => {
|
||||
if (!window.confirm(`Cancel delivery pickup for order ${ord.orderNumber}?`)) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await cancelOrderPickup(ord.id, 'Seller requested cancellation');
|
||||
setNotice(`Delivery pickup cancelled for order ${ord.orderNumber}.`);
|
||||
} catch (e: any) {
|
||||
alert(`Failed to cancel pickup: ${e.message}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (showManifest) {
|
||||
|
|
@ -101,7 +142,7 @@ export const OrdersPage: React.FC = () => {
|
|||
if (showReportDamage) {
|
||||
return (
|
||||
<ReportDamageView
|
||||
order={orders[0]} // using first order as sample since the mockup shows a specific item
|
||||
order={orders[0]}
|
||||
onBack={() => setShowReportDamage(false)}
|
||||
onTabChange={(tab) => {
|
||||
if (tab === 'overall') {
|
||||
|
|
@ -127,7 +168,7 @@ export const OrdersPage: React.FC = () => {
|
|||
<OrderDetailView
|
||||
order={activeOrder}
|
||||
onBack={() => setActiveOrder(null)}
|
||||
onPrintLabel={setSelectedOrderForLabel}
|
||||
onPrintLabel={() => handleFetchPrintLabel(activeOrder)}
|
||||
onReschedule={() => {
|
||||
setActiveOrder(null);
|
||||
setShowReschedule(true);
|
||||
|
|
@ -165,7 +206,7 @@ export const OrdersPage: React.FC = () => {
|
|||
</div>
|
||||
<h1 className="title is-size-3 mb-1" style={{ letterSpacing: '-0.02em', fontWeight: 700 }}>Orders & Shipping</h1>
|
||||
<p className="subtitle is-size-6 has-text-grey mt-1">
|
||||
Manage your incoming customer orders and track pickups.
|
||||
Manage incoming customer orders, dispatch delivery pickups, and print 4x6 shipping labels.
|
||||
</p>
|
||||
</div>
|
||||
<div className="is-flex is-align-items-center gap-3">
|
||||
|
|
@ -173,13 +214,19 @@ export const OrdersPage: React.FC = () => {
|
|||
<Calendar size={16} className="mr-2 has-text-warning-dark" />
|
||||
Reschedule Pickup
|
||||
</button>
|
||||
<button onClick={handlePrintAll} className="button is-rounded is-trad-primary shadow-sm" style={{ height: '40px', fontWeight: 600, fontSize: '0.85rem' }}>
|
||||
<Printer size={16} className="mr-2" />
|
||||
Print Today's Labels ({readyForPickupCount})
|
||||
<button onClick={() => navigate('/dashboard/orders/returns')} className="button is-rounded is-trad-primary shadow-sm" style={{ height: '40px', fontWeight: 600, fontSize: '0.85rem' }}>
|
||||
<RefreshCw size={16} className="mr-2" />
|
||||
Returns Tracker
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{notice && (
|
||||
<div className="notification is-success is-light p-3 mb-4 is-size-7">
|
||||
<span className="has-text-weight-bold">{notice}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Primary Action Tabs */}
|
||||
<div className="is-flex is-align-items-center mb-5 overflow-x-auto pb-2" style={{ gap: '0.5rem', borderBottom: '1px solid var(--trad-border)' }}>
|
||||
<button
|
||||
|
|
@ -188,7 +235,7 @@ export const OrdersPage: React.FC = () => {
|
|||
style={{ fontWeight: 600, ...(activeTab !== 'all' ? { backgroundColor: '#F6F3F2', color: '#564243' } : {}) }}
|
||||
>
|
||||
<Package size={16} className="mr-2" />
|
||||
Overall
|
||||
Overall ({orders.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('ready_to_ship')}
|
||||
|
|
@ -196,7 +243,7 @@ export const OrdersPage: React.FC = () => {
|
|||
style={{ fontWeight: 600, ...(activeTab !== 'ready_to_ship' ? { backgroundColor: '#F6F3F2', color: '#564243' } : {}) }}
|
||||
>
|
||||
<Inbox size={16} className="mr-2" />
|
||||
Ready for pickup
|
||||
Ready for pickup ({readyForPickupCount})
|
||||
</button>
|
||||
<button onClick={() => setShowManifest(true)} className="button is-rounded is-light is-small px-4" style={{ backgroundColor: '#F6F3F2', color: '#564243', fontWeight: 600 }}>
|
||||
<CheckCircle2 size={16} className="mr-2" />
|
||||
|
|
@ -304,17 +351,9 @@ export const OrdersPage: React.FC = () => {
|
|||
</div>
|
||||
<div className="is-flex is-align-items-center gap-2" style={{ flexShrink: 0 }}>
|
||||
<div className="control has-icons-left is-expanded" style={{ maxWidth: '300px', minWidth: '220px' }}>
|
||||
<input className="input is-small is-rounded" type="text" placeholder="Search by order #, buyer, or craft..." style={{ backgroundColor: '#F6F3F2', borderColor: 'transparent' }} />
|
||||
<input className="input is-small is-rounded" type="text" placeholder="Search order #, buyer..." style={{ backgroundColor: '#F6F3F2', borderColor: 'transparent' }} />
|
||||
<span className="icon is-small is-left"><Search size={14} color="#897172" /></span>
|
||||
</div>
|
||||
<div className="select is-small is-rounded">
|
||||
<select style={{ backgroundColor: '#F6F3F2', borderColor: 'transparent', color: '#1c1b1b', fontWeight: 500 }}>
|
||||
<option>All Couriers</option>
|
||||
<option>Delhivery Air</option>
|
||||
<option>Blue Dart Express</option>
|
||||
<option>Tradhox Air</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -326,159 +365,204 @@ export const OrdersPage: React.FC = () => {
|
|||
<th className="has-text-grey is-size-7" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Order & Date</th>
|
||||
<th className="has-text-grey is-size-7" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Customer & City</th>
|
||||
<th className="has-text-grey is-size-7" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Handcrafted Item</th>
|
||||
<th className="has-text-grey is-size-7" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Amount & Payment</th>
|
||||
<th className="has-text-grey is-size-7" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Amount</th>
|
||||
<th className="has-text-grey is-size-7" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Carrier / AWB</th>
|
||||
<th className="has-text-grey is-size-7" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Status</th>
|
||||
<th className="has-text-grey is-size-7 has-text-right" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Action</th>
|
||||
<th className="has-text-grey is-size-7 has-text-right" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredOrders.map((ord) => (
|
||||
<tr key={ord.id} style={{ borderBottom: '1px solid var(--trad-border)' }}>
|
||||
<td className="is-vcentered">
|
||||
<div className="has-text-weight-bold is-size-7" style={{ color: 'var(--trad-maroon)' }}>{ord.orderNumber}</div>
|
||||
<div className="is-size-7 has-text-grey mt-1">{ord.date}</div>
|
||||
</td>
|
||||
<td className="is-vcentered">
|
||||
<div className="has-text-weight-semibold is-size-7" style={{ color: '#1c1b1b' }}>{ord.customerName}</div>
|
||||
<div className="is-size-7 has-text-grey mt-1">{ord.customerCity}, {ord.customerState}</div>
|
||||
</td>
|
||||
<td className="is-vcentered">
|
||||
<div className="is-flex is-align-items-center gap-3">
|
||||
<div style={{ width: '44px', height: '44px', borderRadius: '8px', backgroundColor: '#F6F3F2', flexShrink: 0, overflow: 'hidden' }}>
|
||||
<img src={`https://images.unsplash.com/photo-1610701596007-11502861dcfa?auto=format&fit=crop&w=100&q=80`} alt={ord.productTitle} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="has-text-weight-semibold is-size-7" style={{ color: '#1c1b1b' }}>{ord.productTitle}</div>
|
||||
<div className="is-size-7 has-text-grey mt-1">{ord.quantity} Unit(s)</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="is-vcentered">
|
||||
<div className="has-text-weight-semibold is-size-7" style={{ color: '#1c1b1b' }}>₹{ord.amount.toLocaleString()}</div>
|
||||
<div className="is-size-7 mt-1 is-flex is-align-items-center" style={{ color: '#5dbe73' }}>
|
||||
<CheckCircle2 size={13} className="mr-1" /> Prepaid
|
||||
</div>
|
||||
</td>
|
||||
<td className="is-vcentered">
|
||||
<span className={`tag is-rounded is-small has-text-weight-semibold ${
|
||||
ord.status === 'new' ? 'is-warning is-light' :
|
||||
ord.status === 'ready_to_ship' ? 'is-info is-light' :
|
||||
ord.status === 'dispatched' ? 'is-primary is-light' : 'is-success is-light'
|
||||
}`}>
|
||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: 'currentColor', marginRight: '6px' }}></span>
|
||||
{ord.status.replace('_', ' ').toUpperCase()}
|
||||
</span>
|
||||
</td>
|
||||
<td className="is-vcentered has-text-right">
|
||||
<div className="is-flex is-align-items-center is-justify-content-flex-end gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (ord.status === 'ready_to_ship') setActiveOrder(ord);
|
||||
else if (ord.status !== 'delivered' && ord.status !== 'dispatched') setSelectedOrderForLabel(ord);
|
||||
}}
|
||||
className="button is-small is-rounded is-trad-primary shadow-sm"
|
||||
style={{ fontWeight: 600, fontSize: '0.75rem', height: '32px' }}
|
||||
>
|
||||
{ord.status === 'delivered' ? <span className="mr-1 material-symbols-outlined is-size-6" style={{ fontSize: '15px' }}>description</span> : <Printer size={14} className="mr-1" />}
|
||||
{ord.status === 'delivered' ? 'Receipt' : ord.status === 'dispatched' ? 'Track' : ord.status === 'ready_to_ship' ? 'View Details' : 'Print Label'}
|
||||
</button>
|
||||
<button className="button is-small is-rounded is-white has-text-grey" style={{ width: '32px', height: '32px', padding: 0 }}>
|
||||
<MoreVertical size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
{filteredOrders.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="has-text-centered py-6 has-text-grey">No orders found in this section.</td>
|
||||
</tr>
|
||||
))}
|
||||
) : (
|
||||
filteredOrders.map((ord) => (
|
||||
<tr key={ord.id} style={{ borderBottom: '1px solid var(--trad-border)' }}>
|
||||
<td className="is-vcentered">
|
||||
<div className="has-text-weight-bold is-size-7" style={{ color: 'var(--trad-maroon)' }}>{ord.orderNumber}</div>
|
||||
<div className="is-size-7 has-text-grey mt-1">{ord.date}</div>
|
||||
</td>
|
||||
<td className="is-vcentered">
|
||||
<div className="has-text-weight-semibold is-size-7" style={{ color: '#1c1b1b' }}>{ord.customerName}</div>
|
||||
<div className="is-size-7 has-text-grey mt-1">{ord.customerCity}, {ord.customerState}</div>
|
||||
</td>
|
||||
<td className="is-vcentered">
|
||||
<div className="has-text-weight-semibold is-size-7" style={{ color: '#1c1b1b' }}>{ord.productTitle}</div>
|
||||
<div className="is-size-7 has-text-grey mt-1">{ord.quantity} Unit(s)</div>
|
||||
</td>
|
||||
<td className="is-vcentered">
|
||||
<div className="has-text-weight-semibold is-size-7" style={{ color: '#1c1b1b' }}>₹{ord.amount.toLocaleString()}</div>
|
||||
<div className="is-size-7 mt-1 is-flex is-align-items-center" style={{ color: '#5dbe73' }}>
|
||||
<CheckCircle2 size={13} className="mr-1" /> COD / Prepaid
|
||||
</div>
|
||||
</td>
|
||||
<td className="is-vcentered">
|
||||
<div className="is-size-7 has-text-weight-bold">{ord.courierPartner || 'Shadowfax Logistics'}</div>
|
||||
<div className="is-size-7 has-text-grey mt-1">{ord.awbNumber || 'Pending'}</div>
|
||||
</td>
|
||||
<td className="is-vcentered">
|
||||
<span className={`tag is-rounded is-small has-text-weight-semibold ${
|
||||
ord.status === 'new' ? 'is-warning is-light' :
|
||||
ord.status === 'ready_to_ship' ? 'is-info is-light' :
|
||||
ord.status === 'dispatched' ? 'is-primary is-light' : 'is-success is-light'
|
||||
}`}>
|
||||
{ord.status.replace('_', ' ').toUpperCase()}
|
||||
</span>
|
||||
</td>
|
||||
<td className="is-vcentered has-text-right">
|
||||
<div className="is-flex is-align-items-center is-justify-content-flex-end gap-2">
|
||||
{ord.status === 'new' ? (
|
||||
<button
|
||||
onClick={() => handleAcceptAndShip(ord)}
|
||||
className="button is-small is-rounded is-success"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<CheckCircle2 size={14} className="mr-1" /> Accept & Ship
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handleTrackShipment(ord)}
|
||||
className="button is-small is-rounded is-info is-light"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Send size={14} className="mr-1" /> Track
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleFetchPrintLabel(ord)}
|
||||
className="button is-small is-rounded is-trad-primary shadow-sm"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Printer size={14} className="mr-1" /> Label
|
||||
</button>
|
||||
{ord.status === 'ready_to_ship' && (
|
||||
<button
|
||||
onClick={() => handleCancelPickup(ord)}
|
||||
className="button is-small is-rounded is-danger is-light"
|
||||
title="Cancel Delivery Pickup"
|
||||
>
|
||||
<XCircle size={14} />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination Footer */}
|
||||
<div className="p-4 is-flex is-flex-direction-column is-flex-direction-row-tablet is-align-items-center is-justify-content-space-between" style={{ backgroundColor: '#F6F3F2', borderTop: '1px solid var(--trad-border)' }}>
|
||||
<span className="is-size-7 has-text-grey mb-3 mb-0-tablet">
|
||||
Showing <strong style={{ color: '#1c1b1b' }}>{filteredOrders.length === 0 ? 0 : `1–${filteredOrders.length}`}</strong> of <strong style={{ color: '#1c1b1b' }}>{orders.length}</strong> orders
|
||||
</span>
|
||||
<div className="is-flex is-align-items-center gap-1">
|
||||
<button className="button is-small is-rounded is-white has-text-grey" disabled>Previous</button>
|
||||
<button className="button is-small is-rounded is-trad-primary has-text-weight-bold" style={{ width: '32px', height: '32px', padding: 0 }}>1</button>
|
||||
<button className="button is-small is-rounded is-white has-text-grey" style={{ width: '32px', height: '32px', padding: 0 }}>2</button>
|
||||
<button className="button is-small is-rounded is-white has-text-grey" style={{ width: '32px', height: '32px', padding: 0 }}>3</button>
|
||||
<span className="is-size-7 has-text-grey px-1">...</span>
|
||||
<button className="button is-small is-rounded is-white has-text-grey">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* MODAL 1: 4x6 Thermal Shipping Label View (Faithful to Stitch) */}
|
||||
{/* MODAL: Thermal Shipping Label View */}
|
||||
{selectedOrderForLabel && (
|
||||
<div className="modal is-active">
|
||||
<div className="modal-background" onClick={() => setSelectedOrderForLabel(null)}></div>
|
||||
<div className="modal-card" style={{ maxWidth: '440px' }}>
|
||||
<header className="modal-card-head" style={{ background: '#ffffff', borderBottom: '1px solid var(--trad-border)' }}>
|
||||
<p className="modal-card-title is-size-6 has-text-weight-bold">
|
||||
Thermal 4x6 Shipping Label View
|
||||
Print 4x6 Delivery Shipping Label
|
||||
</p>
|
||||
<button className="delete" aria-label="close" onClick={() => setSelectedOrderForLabel(null)}></button>
|
||||
</header>
|
||||
<section className="modal-card-body p-4 has-text-centered" style={{ background: '#F5F5F5' }}>
|
||||
{/* 4x6 Label Container */}
|
||||
<div className="thermal-label-container has-text-left" style={{ background: '#ffffff', border: '2px dashed #000', padding: '16px', borderRadius: '4px' }}>
|
||||
<div className="is-flex is-justify-content-space-between is-align-items-center mb-2 pb-2" style={{ borderBottom: '2px solid #000' }}>
|
||||
<div>
|
||||
<h4 className="has-text-weight-black is-size-5 mb-0" style={{ letterSpacing: '1px' }}>TRADHOX EXPRESS</h4>
|
||||
<span className="is-size-7 font-weight-bold">PREPAID AIR LOGISTICS</span>
|
||||
</div>
|
||||
<div className="has-text-right">
|
||||
<span className="has-text-weight-black is-size-5">DEL</span>
|
||||
<h4 className="has-text-weight-black is-size-5 mb-0" style={{ letterSpacing: '1px' }}>{selectedOrderForLabel.carrier || 'SHADOWFAX LOGISTICS'}</h4>
|
||||
<span className="is-size-7 font-weight-bold">EXPRESS DELIVERY</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 has-text-centered py-2" style={{ borderBottom: '2px solid #000' }}>
|
||||
<div className="is-size-7 font-mono mb-1">AWB: {selectedOrderForLabel.awbNumber}</div>
|
||||
{/* Barcode Simulation */}
|
||||
<div className="is-flex is-justify-content-center py-2" style={{ background: '#000', height: '38px', letterSpacing: '4px', color: '#fff', fontSize: '10px', alignItems: 'center' }}>
|
||||
||||||| | ||||| |||| || |||||| | ||||| ||||
|
||||
</div>
|
||||
<div className="is-size-7 font-mono mt-1">ORDER ID: {selectedOrderForLabel.orderNumber}</div>
|
||||
<div className="is-size-7 font-mono mb-1">AWB: {selectedOrderForLabel.awb_number}</div>
|
||||
{selectedOrderForLabel.barcode_url ? (
|
||||
<img src={selectedOrderForLabel.barcode_url} alt="QR Code" style={{ width: '120px' }} className="mx-auto" />
|
||||
) : (
|
||||
<div className="is-flex is-justify-content-center py-2" style={{ background: '#000', height: '38px', letterSpacing: '4px', color: '#fff', fontSize: '10px', alignItems: 'center' }}>
|
||||
||||||| | ||||| |||| || |||||| | ||||| ||||
|
||||
</div>
|
||||
)}
|
||||
<div className="is-size-7 font-mono mt-1">ORDER ID: {selectedOrderForLabel.order_id}</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 pb-2" style={{ borderBottom: '1px solid #000', fontSize: '11px', lineHeight: 1.4 }}>
|
||||
<div className="has-text-weight-bold mb-1">DELIVER TO:</div>
|
||||
<div className="has-text-weight-black is-size-6">{selectedOrderForLabel.customerName}</div>
|
||||
<div>Flat 402, Heritage Residency, MG Road</div>
|
||||
<div>{selectedOrderForLabel.customerCity}, {selectedOrderForLabel.customerState} - 400001</div>
|
||||
<div>Phone: +91 98200 XXXXX</div>
|
||||
<div className="has-text-weight-black is-size-6">{selectedOrderForLabel.recipient?.name}</div>
|
||||
<div>{selectedOrderForLabel.recipient?.address}</div>
|
||||
<div>Phone: {selectedOrderForLabel.recipient?.phone}</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 pb-2" style={{ borderBottom: '1px solid #000', fontSize: '11px', lineHeight: 1.4 }}>
|
||||
<div className="has-text-weight-bold mb-1">RETURN ADDRESS (SELLER GUILD):</div>
|
||||
<div>Raghavendra Artisan Guild</div>
|
||||
<div>42, Shilpkala Marg, Sanganer Craft Cluster</div>
|
||||
<div>Jaipur, Rajasthan - 302029</div>
|
||||
<div className="has-text-weight-bold mb-1">DISPATCHED FROM (SELLER):</div>
|
||||
<div>{selectedOrderForLabel.sender?.name}</div>
|
||||
<div>{selectedOrderForLabel.sender?.address}</div>
|
||||
<div>Phone: {selectedOrderForLabel.sender?.phone}</div>
|
||||
</div>
|
||||
|
||||
<div className="is-flex is-justify-content-space-between is-align-items-center" style={{ fontSize: '11px' }}>
|
||||
<div>
|
||||
<span>Dead Wt: 1.20 Kg</span> | <span>Qty: {selectedOrderForLabel.quantity}</span>
|
||||
</div>
|
||||
<div className="has-text-weight-bold">
|
||||
GI TAG CRAFT
|
||||
</div>
|
||||
<div>Item: {selectedOrderForLabel.package_details?.item}</div>
|
||||
<div className="has-text-weight-bold">Collectible COD: ₹{selectedOrderForLabel.package_details?.collectible_amount}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<footer className="modal-card-foot" style={{ background: '#ffffff', borderTop: '1px solid var(--trad-border)', justifyContent: 'space-between' }}>
|
||||
<button className="button is-small is-rounded" onClick={() => setSelectedOrderForLabel(null)}>Close</button>
|
||||
<button className="button is-trad-primary is-small is-rounded" onClick={() => alert('Printing to thermal 4x6 label printer...')}>
|
||||
<button className="button is-trad-primary is-small is-rounded" onClick={() => window.print()}>
|
||||
<Printer size={14} className="mr-1" />
|
||||
<span>Print to Thermal Printer</span>
|
||||
<span>Print Thermal Label</span>
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* MODAL: Live Shipment Tracking */}
|
||||
{trackingInfo && (
|
||||
<div className="modal is-active">
|
||||
<div className="modal-background" onClick={() => setTrackingInfo(null)}></div>
|
||||
<div className="modal-card" style={{ maxWidth: '500px' }}>
|
||||
<header className="modal-card-head" style={{ backgroundColor: '#fff' }}>
|
||||
<p className="modal-card-title is-size-6 has-text-weight-bold">
|
||||
Live Shipment Tracking (Order #{trackingOrderNumber})
|
||||
</p>
|
||||
<button className="delete" aria-label="close" onClick={() => setTrackingInfo(null)}></button>
|
||||
</header>
|
||||
<section className="modal-card-body">
|
||||
<div className="content is-size-7">
|
||||
<div className="p-3 mb-3" style={{ background: '#F6F3F2', borderRadius: '8px' }}>
|
||||
<div><strong>AWB Number:</strong> {trackingInfo.awb_number}</div>
|
||||
<div><strong>Current Status:</strong> <span className="tag is-info is-light">{trackingInfo.current_status || 'out_for_delivery'}</span></div>
|
||||
{trackingInfo.rider_details && (
|
||||
<div className="mt-2 pt-2" style={{ borderTop: '1px solid #ddd' }}>
|
||||
<div><strong>Assigned Rider:</strong> {trackingInfo.rider_details.name}</div>
|
||||
<div><strong>Rider Phone:</strong> {trackingInfo.rider_details.phone}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{trackingInfo.tracking_history && (
|
||||
<div>
|
||||
<h5 className="is-size-7 font-weight-bold mb-2">Tracking History Timeline:</h5>
|
||||
<ul>
|
||||
{trackingInfo.tracking_history.map((h: any, idx: number) => (
|
||||
<li key={idx} className="mb-1">
|
||||
<strong>{h.status}</strong> - {h.location || 'Hub'} ({h.timestamp})
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<footer className="modal-card-foot" style={{ backgroundColor: '#fff', justifyContent: 'flex-end' }}>
|
||||
<button className="button is-small" onClick={() => setTrackingInfo(null)}>Close</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,18 +1,104 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useSeller } from '../../context/SellerContext';
|
||||
import {
|
||||
Search,
|
||||
Printer,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ShieldAlert,
|
||||
Send
|
||||
Send,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
QrCode,
|
||||
FileText,
|
||||
AlertCircle
|
||||
} from 'lucide-react';
|
||||
|
||||
export const ReturnsTrackerPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { returnsList, actionReturn, trackReturn, updateReturnQC, getReturnLabel } = useSeller();
|
||||
|
||||
const reverseShipments: any[] = [];
|
||||
const [activeReturn, setActiveReturn] = useState<any | null>(null);
|
||||
const [trackingInfo, setTrackingInfo] = useState<any | null>(null);
|
||||
const [labelData, setLabelData] = useState<any | null>(null);
|
||||
const [qcModalReturn, setQcModalReturn] = useState<any | null>(null);
|
||||
|
||||
const [qcSerialNumber, setQcSerialNumber] = useState('SN-9988112233');
|
||||
const [qcBrandName, setQcBrandName] = useState('Authentic Artisan');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
|
||||
const handleApproveReturn = async (id: string | number) => {
|
||||
setIsLoading(true);
|
||||
setNotice(null);
|
||||
try {
|
||||
await actionReturn(String(id), 'Approved');
|
||||
setNotice('Return approved successfully! Customer return pickup & reverse AWB generated.');
|
||||
} catch (e: any) {
|
||||
alert(`Failed to approve return: ${e.message}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRejectReturn = async (id: string | number) => {
|
||||
setIsLoading(true);
|
||||
setNotice(null);
|
||||
try {
|
||||
await actionReturn(String(id), 'Rejected');
|
||||
setNotice('Return request rejected.');
|
||||
} catch (e: any) {
|
||||
alert(`Failed to reject return: ${e.message}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTrackReturn = async (returnObj: any) => {
|
||||
setIsLoading(true);
|
||||
setNotice(null);
|
||||
try {
|
||||
const res = await trackReturn(returnObj.return_id || String(returnObj.id));
|
||||
setTrackingInfo(res.data || res);
|
||||
setActiveReturn(returnObj);
|
||||
} catch (e: any) {
|
||||
alert(`Tracking failed: ${e.message}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrintLabel = async (returnObj: any) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await getReturnLabel(returnObj.return_id || String(returnObj.id));
|
||||
setLabelData(res.label || res);
|
||||
} catch (e: any) {
|
||||
alert(`Failed to fetch label: ${e.message}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveQC = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!qcModalReturn) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await updateReturnQC(qcModalReturn.return_id || String(qcModalReturn.id), {
|
||||
qc_check: true,
|
||||
serial_number: qcSerialNumber,
|
||||
brand_name: qcBrandName
|
||||
});
|
||||
setNotice('Doorstep Quality Check (QC) specifications updated for rider!');
|
||||
setQcModalReturn(null);
|
||||
} catch (e: any) {
|
||||
alert(`Failed to update QC: ${e.message}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="returns-tracker-page" style={{ maxWidth: '1400px', margin: '0 auto' }}>
|
||||
|
|
@ -26,39 +112,34 @@ export const ReturnsTrackerPage: React.FC = () => {
|
|||
</nav>
|
||||
<h1 className="title is-size-2 mb-2" style={{ letterSpacing: '-0.02em', fontWeight: 700, color: '#1c1b1b' }}>Return Order Tracker</h1>
|
||||
<p className="is-size-6 has-text-grey" style={{ maxWidth: '800px', color: '#564243' }}>
|
||||
Track reverse logistics sweeps, in-transit customer returns, unboxing quality checks, and 24-hour transit damage claims across artisan craft hubs.
|
||||
Track reverse logistics sweeps, in-transit customer returns, doorstep Quality Checks (QC), and return delivery labels.
|
||||
</p>
|
||||
</div>
|
||||
<div className="is-flex is-align-items-center gap-3">
|
||||
<button
|
||||
onClick={() => navigate('/dashboard/orders')} // Ideally goes to report damage view
|
||||
onClick={() => navigate('/dashboard/orders')}
|
||||
className="button is-rounded is-trad-primary shadow-sm"
|
||||
style={{ height: '44px', fontWeight: 600, fontSize: '0.9rem', padding: '0 1.5rem', backgroundColor: '#791b2c', color: '#fff' }}
|
||||
>
|
||||
<ShieldAlert size={18} className="mr-2" />
|
||||
File Damage Claim
|
||||
File Transit Damage Claim
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{notice && (
|
||||
<div className="notification is-success is-light p-3 mb-4 is-size-7">
|
||||
<span className="has-text-weight-bold">{notice}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Orders Table Container */}
|
||||
<div className="card" style={{ borderRadius: '16px', border: '1px solid var(--trad-border)', boxShadow: '0 2px 8px rgba(0,0,0,0.02)', overflow: 'hidden', backgroundColor: '#ffffff' }}>
|
||||
{/* Table Filters Header */}
|
||||
<div className="p-4 is-flex is-flex-wrap-wrap is-align-items-center is-justify-content-space-between" style={{ borderBottom: '1px solid var(--trad-border)', gap: '1rem' }}>
|
||||
<div className="is-flex is-align-items-center" style={{ gap: '0.5rem' }}>
|
||||
<span className="has-text-weight-bold" style={{ color: '#1c1b1b' }}>Reverse Shipments</span>
|
||||
<span className="tag is-rounded" style={{ backgroundColor: '#f0eded', color: '#564243', fontWeight: 600 }}>{reverseShipments.length} Active</span>
|
||||
</div>
|
||||
<div className="is-flex is-align-items-center gap-3">
|
||||
<span className="is-size-7 has-text-grey">Scroll table</span>
|
||||
<div className="is-flex gap-1">
|
||||
<button className="button is-small is-rounded is-white" style={{ width: '32px', height: '32px', padding: 0, border: '1px solid #e5e2e1' }}>
|
||||
<ChevronLeft size={16} />
|
||||
</button>
|
||||
<button className="button is-small is-rounded is-white" style={{ width: '32px', height: '32px', padding: 0, border: '1px solid #e5e2e1' }}>
|
||||
<ChevronRight size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<span className="tag is-rounded" style={{ backgroundColor: '#f0eded', color: '#564243', fontWeight: 600 }}>{returnsList.length} Active</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -67,82 +148,219 @@ export const ReturnsTrackerPage: React.FC = () => {
|
|||
<table className="table is-fullwidth is-hoverable" style={{ borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ backgroundColor: '#ffffff' }}>
|
||||
<th className="has-text-grey is-size-7 pb-4" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Return<br/>& Date</th>
|
||||
<th className="has-text-grey is-size-7 pb-4" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Order & Craft Item</th>
|
||||
<th className="has-text-grey is-size-7 pb-4" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Customer &<br/>City</th>
|
||||
<th className="has-text-grey is-size-7 pb-4" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Return<br/>Reason</th>
|
||||
<th className="has-text-grey is-size-7 pb-4" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Return ID & Order</th>
|
||||
<th className="has-text-grey is-size-7 pb-4" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Item & Customer</th>
|
||||
<th className="has-text-grey is-size-7 pb-4" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Return Reason</th>
|
||||
<th className="has-text-grey is-size-7 pb-4" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Status</th>
|
||||
<th className="has-text-grey is-size-7 pb-4" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Carrier /<br/>AWB</th>
|
||||
<th className="has-text-grey is-size-7 pb-4 has-text-right" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Action</th>
|
||||
<th className="has-text-grey is-size-7 pb-4" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Reverse AWB</th>
|
||||
<th className="has-text-grey is-size-7 pb-4 has-text-right" style={{ textTransform: 'uppercase', letterSpacing: '0.04em', borderBottom: '1px solid #e5e2e1' }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{reverseShipments.map((rtn) => (
|
||||
<tr key={rtn.id} style={{ borderBottom: '1px solid #e5e2e1' }}>
|
||||
<td className="is-vcentered py-4">
|
||||
<div className="has-text-weight-bold is-size-7" style={{ color: '#5a0118' }}>{rtn.id}</div>
|
||||
<div className="is-size-7 mt-1" style={{ color: '#564243' }}>{rtn.date}</div>
|
||||
</td>
|
||||
<td className="is-vcentered py-4">
|
||||
<div className="is-flex is-align-items-center gap-3">
|
||||
<div style={{ width: '40px', height: '40px', borderRadius: '50%', backgroundColor: '#F6F3F2', flexShrink: 0, overflow: 'hidden' }}>
|
||||
<img src={rtn.imageUrl} alt={rtn.title} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="has-text-weight-semibold is-size-7" style={{ color: '#1c1b1b' }}>{rtn.title}</div>
|
||||
<div className="is-size-7 mt-1" style={{ color: '#564243' }}>{rtn.sku} · {rtn.quantity} {rtn.quantity === 1 ? 'Unit' : 'Units'} (₹{rtn.amount.toLocaleString()})</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="is-vcentered py-4">
|
||||
<div className="is-size-7" style={{ color: '#1c1b1b' }}>{rtn.customerName}</div>
|
||||
<div className="is-size-7 mt-1 is-flex is-align-items-center" style={{ color: '#564243' }}>
|
||||
<span className="material-symbols-outlined mr-1" style={{ fontSize: '14px' }}>location_on</span>
|
||||
{rtn.customerCity} ({rtn.customerPin})
|
||||
</div>
|
||||
</td>
|
||||
<td className="is-vcentered py-4">
|
||||
<div className="is-size-7 is-flex is-align-items-center" style={{ color: rtn.isDamage ? '#ba1a1a' : '#1c1b1b' }}>
|
||||
{rtn.isDamage && <span className="material-symbols-outlined mr-1" style={{ fontSize: '14px' }}>warning</span>}
|
||||
{rtn.returnReason}
|
||||
</div>
|
||||
<div className="is-size-7 mt-1" style={{ color: rtn.reasonColor || '#564243' }}>{rtn.reasonSub}</div>
|
||||
</td>
|
||||
<td className="is-vcentered py-4">
|
||||
<span className={`tag is-rounded has-text-weight-semibold ${rtn.statusColor}`} style={{ padding: '0 12px', height: '32px', fontSize: '12px' }}>
|
||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: 'currentColor', marginRight: '6px' }}></span>
|
||||
{rtn.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="is-vcentered py-4">
|
||||
<div className="is-size-7" style={{ color: '#1c1b1b' }}>{rtn.carrier}</div>
|
||||
<div className="is-size-7 mt-1" style={{ color: '#564243' }}>{rtn.awb}</div>
|
||||
</td>
|
||||
<td className="is-vcentered py-4 has-text-right">
|
||||
<button className="button is-small is-rounded is-light px-4" style={{ height: '36px', fontWeight: 600, backgroundColor: '#f6f3f2', border: 'none', color: '#1c1b1b' }}>
|
||||
<Send size={14} className="mr-2" />
|
||||
Track
|
||||
</button>
|
||||
{returnsList.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="has-text-centered py-6 has-text-grey">
|
||||
<AlertCircle size={32} className="mx-auto mb-2" />
|
||||
<div>No active return requests found.</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
) : (
|
||||
returnsList.map((rtn) => (
|
||||
<tr key={rtn.id} style={{ borderBottom: '1px solid #e5e2e1' }}>
|
||||
<td className="is-vcentered py-4">
|
||||
<div className="has-text-weight-bold is-size-7" style={{ color: '#5a0118' }}>{rtn.return_id || `RET-${rtn.id}`}</div>
|
||||
<div className="is-size-7 mt-1" style={{ color: '#564243' }}>Order: {rtn.order_id}</div>
|
||||
</td>
|
||||
<td className="is-vcentered py-4">
|
||||
<div className="has-text-weight-semibold is-size-7" style={{ color: '#1c1b1b' }}>{rtn.item}</div>
|
||||
<div className="is-size-7 mt-1" style={{ color: '#564243' }}>Customer: {rtn.customer}</div>
|
||||
</td>
|
||||
<td className="is-vcentered py-4">
|
||||
<div className="is-size-7" style={{ color: '#1c1b1b' }}>{rtn.reason}</div>
|
||||
</td>
|
||||
<td className="is-vcentered py-4">
|
||||
<span className={`tag is-rounded has-text-weight-semibold ${
|
||||
rtn.status === 'Approved' || rtn.status === 'In Transit' ? 'is-info is-light' :
|
||||
rtn.status === 'Rejected' ? 'is-danger is-light' : 'is-warning is-light'
|
||||
}`} style={{ padding: '0 12px', height: '28px', fontSize: '12px' }}>
|
||||
{rtn.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="is-vcentered py-4">
|
||||
<div className="is-size-7 has-text-weight-bold" style={{ color: '#1c1b1b' }}>{rtn.returning_tracking || 'Pending Pickup'}</div>
|
||||
<div className="is-size-7 mt-1" style={{ color: '#564243' }}>Carrier: Shadowfax</div>
|
||||
</td>
|
||||
<td className="is-vcentered py-4 has-text-right">
|
||||
<div className="is-flex is-align-items-center is-justify-content-flex-end gap-2">
|
||||
{rtn.status === 'Pending Approval' ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handleApproveReturn(rtn.id)}
|
||||
className="button is-small is-success is-rounded"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<CheckCircle2 size={14} className="mr-1" /> Approve
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleRejectReturn(rtn.id)}
|
||||
className="button is-small is-danger is-light is-rounded"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<XCircle size={14} className="mr-1" /> Reject
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setQcModalReturn(rtn)}
|
||||
className="button is-small is-light is-rounded"
|
||||
title="Update Doorstep QC"
|
||||
>
|
||||
QC Specs
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleTrackReturn(rtn)}
|
||||
className="button is-small is-info is-light is-rounded"
|
||||
>
|
||||
<Send size={14} className="mr-1" /> Track
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handlePrintLabel(rtn)}
|
||||
className="button is-small is-trad-primary is-rounded"
|
||||
>
|
||||
<Printer size={14} className="mr-1" /> Label
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pagination Footer */}
|
||||
<div className="p-4 is-flex is-align-items-center is-justify-content-space-between" style={{ backgroundColor: '#fcf9f8', borderTop: '1px solid #e5e2e1' }}>
|
||||
<span className="is-size-7" style={{ color: '#564243' }}>
|
||||
Showing {reverseShipments.length} of {reverseShipments.length} active reverse shipments
|
||||
</span>
|
||||
<div className="is-flex is-align-items-center gap-1">
|
||||
<button className="button is-small is-rounded is-white" style={{ border: 'none', color: '#564243', fontWeight: 500, backgroundColor: 'transparent' }}>Previous</button>
|
||||
<button className="button is-small is-rounded is-trad-primary has-text-weight-bold" style={{ width: '32px', height: '32px', padding: 0, backgroundColor: '#791b2c', color: '#fff', border: 'none' }}>1</button>
|
||||
<button className="button is-small is-rounded is-white" style={{ width: '32px', height: '32px', padding: 0, border: 'none', color: '#564243', backgroundColor: 'transparent' }}>2</button>
|
||||
<button className="button is-small is-rounded is-white" style={{ border: 'none', color: '#564243', fontWeight: 500, backgroundColor: 'transparent' }}>Next</button>
|
||||
{/* QC Modal */}
|
||||
{qcModalReturn && (
|
||||
<div className="modal is-active">
|
||||
<div className="modal-background" onClick={() => setQcModalReturn(null)}></div>
|
||||
<div className="modal-card" style={{ maxWidth: '500px' }}>
|
||||
<header className="modal-card-head" style={{ backgroundColor: '#fff' }}>
|
||||
<p className="modal-card-title is-size-6 has-text-weight-bold">
|
||||
Configure Doorstep Quality Check (QC)
|
||||
</p>
|
||||
<button className="delete" aria-label="close" onClick={() => setQcModalReturn(null)}></button>
|
||||
</header>
|
||||
<section className="modal-card-body">
|
||||
<form onSubmit={handleSaveQC}>
|
||||
<div className="field mb-3">
|
||||
<label className="label is-size-7">Return ID</label>
|
||||
<input className="input is-small" value={qcModalReturn.return_id || qcModalReturn.id} disabled />
|
||||
</div>
|
||||
<div className="field mb-3">
|
||||
<label className="label is-size-7">Item Name</label>
|
||||
<input className="input is-small" value={qcModalReturn.item} disabled />
|
||||
</div>
|
||||
<div className="field mb-3">
|
||||
<label className="label is-size-7">Brand Name for Verification</label>
|
||||
<input
|
||||
className="input is-small"
|
||||
value={qcBrandName}
|
||||
onChange={e => setQcBrandName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="field mb-4">
|
||||
<label className="label is-size-7">Mandatory Serial / IMEI / Craft Tag Number</label>
|
||||
<input
|
||||
className="input is-small"
|
||||
value={qcSerialNumber}
|
||||
onChange={e => setQcSerialNumber(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="notification is-warning is-light is-size-7">
|
||||
Rider will verify physical condition, brand label, and serial number at doorstep before accepting return package.
|
||||
</div>
|
||||
<div className="buttons is-right">
|
||||
<button type="button" className="button is-small" onClick={() => setQcModalReturn(null)}>Cancel</button>
|
||||
<button type="submit" className={`button is-small is-trad-primary ${isLoading ? 'is-loading' : ''}`}>Save QC Specs</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tracking Modal */}
|
||||
{trackingInfo && (
|
||||
<div className="modal is-active">
|
||||
<div className="modal-background" onClick={() => setTrackingInfo(null)}></div>
|
||||
<div className="modal-card" style={{ maxWidth: '500px' }}>
|
||||
<header className="modal-card-head" style={{ backgroundColor: '#fff' }}>
|
||||
<p className="modal-card-title is-size-6 has-text-weight-bold">
|
||||
Live Return Shipment Tracking (Shadowfax V4)
|
||||
</p>
|
||||
<button className="delete" aria-label="close" onClick={() => setTrackingInfo(null)}></button>
|
||||
</header>
|
||||
<section className="modal-card-body">
|
||||
<div className="content is-size-7">
|
||||
<div className="p-3 mb-3" style={{ background: '#F6F3F2', borderRadius: '8px' }}>
|
||||
<div><strong>Return ID:</strong> {trackingInfo.client_request_id || activeReturn?.return_id}</div>
|
||||
<div><strong>Status:</strong> {trackingInfo.current_status || 'picked_up'}</div>
|
||||
<div><strong>Doorstep QC Status:</strong> <span className="tag is-success is-light">{trackingInfo.qc_status || 'PASSED'}</span></div>
|
||||
{trackingInfo.rider_details && (
|
||||
<div className="mt-2 pt-2" style={{ borderTop: '1px solid #ddd' }}>
|
||||
<div><strong>Rider Name:</strong> {trackingInfo.rider_details.name}</div>
|
||||
<div><strong>Contact:</strong> {trackingInfo.rider_details.phone}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<footer className="modal-card-foot" style={{ backgroundColor: '#fff', justifyContent: 'flex-end' }}>
|
||||
<button className="button is-small" onClick={() => setTrackingInfo(null)}>Close</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Return Label Modal */}
|
||||
{labelData && (
|
||||
<div className="modal is-active">
|
||||
<div className="modal-background" onClick={() => setLabelData(null)}></div>
|
||||
<div className="modal-card" style={{ maxWidth: '440px' }}>
|
||||
<header className="modal-card-head" style={{ backgroundColor: '#fff' }}>
|
||||
<p className="modal-card-title is-size-6 has-text-weight-bold">
|
||||
Print Return Pickup Label
|
||||
</p>
|
||||
<button className="delete" aria-label="close" onClick={() => setLabelData(null)}></button>
|
||||
</header>
|
||||
<section className="modal-card-body p-4">
|
||||
<div className="p-4" style={{ background: '#fff', border: '2px dashed #000', borderRadius: '4px' }}>
|
||||
<div className="has-text-weight-bold is-size-6 mb-1">{labelData.title || 'CUSTOMER RETURN PICKUP LABEL'}</div>
|
||||
<div className="is-size-7 mb-2">Carrier: {labelData.carrier} | AWB: {labelData.awb_number}</div>
|
||||
<div className="is-size-7 mb-2"><strong>Return ID:</strong> {labelData.return_id}</div>
|
||||
<div className="is-size-7 mb-2"><strong>Pickup From:</strong> {labelData.pickup_from?.name} ({labelData.pickup_from?.phone})</div>
|
||||
<div className="is-size-7 mb-2"><strong>Return To:</strong> {labelData.return_to?.name}</div>
|
||||
{labelData.barcode_url && (
|
||||
<div className="has-text-centered my-3">
|
||||
<img src={labelData.barcode_url} alt="QR Barcode" style={{ width: '120px' }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<footer className="modal-card-foot" style={{ backgroundColor: '#fff', justifyContent: 'space-between' }}>
|
||||
<button className="button is-small" onClick={() => setLabelData(null)}>Close</button>
|
||||
<button className="button is-small is-trad-primary" onClick={() => window.print()}>
|
||||
<Printer size={14} className="mr-1" /> Print Label
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,15 +1,22 @@
|
|||
const BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://betapartner-api.tradhox.com/api';
|
||||
const BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api';
|
||||
|
||||
const getAuthHeaders = () => {
|
||||
const getAuthHeaders = (isMultipart = false) => {
|
||||
const userStr = localStorage.getItem('tradhox_user_auth');
|
||||
const headers: Record<string, string> = {};
|
||||
if (userStr) {
|
||||
const auth = JSON.parse(userStr);
|
||||
return {
|
||||
'Authorization': `Bearer ${auth.access_token}`,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
try {
|
||||
const auth = JSON.parse(userStr);
|
||||
if (auth.access_token) {
|
||||
headers['Authorization'] = `Bearer ${auth.access_token}`;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse auth token:', e);
|
||||
}
|
||||
}
|
||||
return { 'Content-Type': 'application/json' };
|
||||
if (!isMultipart) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
return headers;
|
||||
};
|
||||
|
||||
export const api = {
|
||||
|
|
@ -99,6 +106,7 @@ export const api = {
|
|||
return res.json();
|
||||
},
|
||||
|
||||
// Profile & Settings
|
||||
getProfile: async () => {
|
||||
const res = await fetch(`${BASE_URL}/profile/`, {
|
||||
method: 'GET',
|
||||
|
|
@ -108,7 +116,6 @@ export const api = {
|
|||
return res.json();
|
||||
},
|
||||
|
||||
// Profile
|
||||
updateProfile: async (profileData: any) => {
|
||||
const res = await fetch(`${BASE_URL}/profile/`, {
|
||||
method: 'PUT',
|
||||
|
|
@ -119,6 +126,16 @@ export const api = {
|
|||
return res.json();
|
||||
},
|
||||
|
||||
submitGstin: async (gstin: string) => {
|
||||
const res = await fetch(`${BASE_URL}/profile/submit-gstin/`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ gstin })
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to verify GSTIN');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
// Products
|
||||
getProducts: async () => {
|
||||
const res = await fetch(`${BASE_URL}/products/`, { headers: getAuthHeaders() });
|
||||
|
|
@ -136,6 +153,16 @@ export const api = {
|
|||
return res.json();
|
||||
},
|
||||
|
||||
updateProduct: async (id: string | number, productData: any) => {
|
||||
const res = await fetch(`${BASE_URL}/products/${id}/`, {
|
||||
method: 'PUT',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(productData)
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to update product');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
deleteProduct: async (id: string | number) => {
|
||||
const res = await fetch(`${BASE_URL}/products/${id}/`, {
|
||||
method: 'DELETE',
|
||||
|
|
@ -145,7 +172,21 @@ export const api = {
|
|||
return res.ok;
|
||||
},
|
||||
|
||||
// Orders
|
||||
bulkUploadProducts: async (data: FormData | { products: any[] }) => {
|
||||
const isFormData = data instanceof FormData;
|
||||
const res = await fetch(`${BASE_URL}/products/bulk-upload/`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(isFormData),
|
||||
body: isFormData ? data : JSON.stringify(data)
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errData = await res.json().catch(() => ({}));
|
||||
throw new Error(errData.error || errData.message || 'Bulk upload failed');
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
|
||||
// Orders & Forward Shipping
|
||||
getOrders: async () => {
|
||||
const res = await fetch(`${BASE_URL}/orders/`, { headers: getAuthHeaders() });
|
||||
if (!res.ok) throw new Error('Failed to fetch orders');
|
||||
|
|
@ -161,10 +202,143 @@ export const api = {
|
|||
return res.json();
|
||||
},
|
||||
|
||||
rejectOrder: async (id: string | number) => {
|
||||
const res = await fetch(`${BASE_URL}/orders/${id}/reject/`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to reject order');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
createOrderPickup: async (id: string | number, details?: any) => {
|
||||
const res = await fetch(`${BASE_URL}/orders/${id}/create_pickup/`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(details || {})
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to create pickup');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
trackOrder: async (id: string | number, awb?: string) => {
|
||||
const query = awb ? `?awb=${awb}` : '';
|
||||
const res = await fetch(`${BASE_URL}/orders/${id}/track/${query}`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to track shipment');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
cancelOrderPickup: async (id: string | number, reason = 'Seller requested cancellation') => {
|
||||
const res = await fetch(`${BASE_URL}/orders/${id}/cancel_pickup/`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ reason })
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to cancel pickup');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
getOrderLabel: async (id: string | number) => {
|
||||
const res = await fetch(`${BASE_URL}/orders/${id}/print_label/`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to fetch delivery label');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
// Returns & Reverse Shipping with QC
|
||||
getReturns: async () => {
|
||||
const res = await fetch(`${BASE_URL}/returns/`, { headers: getAuthHeaders() });
|
||||
if (!res.ok) throw new Error('Failed to fetch return requests');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
actionReturn: async (id: string | number, action: 'Approved' | 'Rejected') => {
|
||||
const res = await fetch(`${BASE_URL}/returns/${id}/action/`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ action })
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to process return request');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
createReturnPickup: async (id: string | number, details?: any) => {
|
||||
const res = await fetch(`${BASE_URL}/returns/${id}/create_return_pickup/`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(details || {})
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to create return pickup');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
trackReturn: async (id: string | number) => {
|
||||
const res = await fetch(`${BASE_URL}/returns/${id}/track/`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to track return pickup');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
updateReturnQC: async (id: string | number, qcDetails: { qc_check?: boolean; serial_number?: string; brand_name?: string; item_name?: string }) => {
|
||||
const res = await fetch(`${BASE_URL}/returns/${id}/update_qc/`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(qcDetails)
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to update return QC');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
getReturnLabel: async (id: string | number) => {
|
||||
const res = await fetch(`${BASE_URL}/returns/${id}/print_label/`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to fetch return label');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
// Serviceability
|
||||
checkForwardServiceability: async (pincodes: string) => {
|
||||
const res = await fetch(`${BASE_URL}/shipping/forward/serviceability/`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ pincodes })
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to check serviceability');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
checkReverseServiceability: async (pincodes: string) => {
|
||||
const res = await fetch(`${BASE_URL}/shipping/reverse/serviceability/`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ pincodes })
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to check reverse serviceability');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
// Wallet
|
||||
getWallet: async () => {
|
||||
const res = await fetch(`${BASE_URL}/wallet/`, { headers: getAuthHeaders() });
|
||||
if (!res.ok) throw new Error('Failed to fetch wallet');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
withdrawWallet: async (amount: number | string) => {
|
||||
const res = await fetch(`${BASE_URL}/wallet/withdraw/`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ amount: String(amount) })
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errData = await res.json().catch(() => ({}));
|
||||
throw new Error(errData.error || errData.message || 'Withdrawal failed');
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue