feat: initialize Django shipping service project with Shadowfax API integration and webhook simulator
Some checks failed
Deploy Beta (NATIVE) / deploy (push) Failing after 39s

This commit is contained in:
vickytechkey 2026-09-25 21:40:57 +05:30
commit 5c85198e8b
43 changed files with 8406 additions and 0 deletions

144
.forgejo/workflows/beta.yml Normal file
View file

@ -0,0 +1,144 @@
name: Deploy Beta (NATIVE)
on:
push:
branches:
- beta
jobs:
deploy:
runs-on: ubuntu-latest
container:
image: node:22-alpine
steps:
- name: Install git
run: apk add --no-cache git
- name: Checkout Code
uses: https://github.com/actions/checkout@v4
- name: Generate CloudWatch and Nginx Configs Locally
run: |
mkdir -p tmp
cat << 'EOF' > tmp/amazon-cloudwatch-agent.json
{
"agent": { "metrics_collection_interval": 60, "run_as_user": "root" },
"metrics": {
"metrics_collected": {
"disk": { "measurement": ["used_percent"], "metrics_collection_interval": 60, "resources": ["/"] },
"mem": { "measurement": ["mem_used_percent"], "metrics_collection_interval": 60 }
}
},
"logs": {
"logs_collected": {
"files": {
"collect_list": [
{ "file_path": "/home/ubuntu/shippingservice/logs/app.log", "log_group_name": "/shippingservice/app", "log_stream_name": "{instance_id}", "retention_in_days": 30 },
{ "file_path": "/home/ubuntu/shippingservice/logs/error.log", "log_group_name": "/shippingservice/error", "log_stream_name": "{instance_id}", "retention_in_days": 30 },
{ "file_path": "/home/ubuntu/shippingservice/logs/gunicorn-beta-access.log", "log_group_name": "/shippingservice/gunicorn-access", "log_stream_name": "{instance_id}", "retention_in_days": 30 },
{ "file_path": "/home/ubuntu/shippingservice/logs/gunicorn-beta-error.log", "log_group_name": "/shippingservice/gunicorn-error", "log_stream_name": "{instance_id}", "retention_in_days": 30 }
]
}
}
}
}
EOF
cat << 'EOF' > tmp/nginx-proxy.conf
server {
server_name beta-shipping.tradhox.com shipping.tradhox.com betashipping-api.tradhox.com shippingservice-api.tradhox.com shippingservice.tipro.in;
location / {
proxy_pass http://127.0.0.1:8086;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/beta-shipping.tradhox.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/beta-shipping.tradhox.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
server {
listen 80;
server_name beta-shipping.tradhox.com shipping.tradhox.com betashipping-api.tradhox.com shippingservice-api.tradhox.com shippingservice.tipro.in;
return 301 https://$host$request_uri;
}
EOF
- name: Package Application
run: |
tar -czf app.tar.gz --exclude=.git --exclude=tmp .
- name: Deploy to Beta EC2
run: |
apk add --no-cache openssh-client aws-cli
mkdir -p ~/.ssh
echo "${{ secrets.BETA_EC2_SSH_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
# Copy app archive and configuration files
scp -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa app.tar.gz tmp/amazon-cloudwatch-agent.json tmp/nginx-proxy.conf shippingservice-beta.service ${{ secrets.BETA_EC2_USERNAME }}@${{ secrets.BETA_EC2_HOST }}:/tmp/
# Execute deployment commands on remote server
ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa ${{ secrets.BETA_EC2_USERNAME }}@${{ secrets.BETA_EC2_HOST }} << 'EOF'
set -e
# ── 2. Install Host Dependencies ──────────────────────────────
sudo apt-get update
sudo apt-get install -y python3-pip python3-venv python3-dev libpq-dev build-essential nginx certbot python3-certbot-nginx
# ── 3. Extract codebase to target directory ───────────────────
sudo mkdir -p /home/ubuntu/shippingservice
sudo chown -R ubuntu:ubuntu /home/ubuntu/shippingservice
tar -xzf /tmp/app.tar.gz -C /home/ubuntu/shippingservice
# ── 4. Set up python virtual environment & dependencies ────────
cd /home/ubuntu/shippingservice
mkdir -p logs
python3 -m venv venv
./venv/bin/pip install --upgrade pip
./venv/bin/pip install -r requirements.txt
# ── 5. Configure environment variables ─────────────────────────
echo "DEBUG=0" > .env.beta
echo "ENVIRONMENT=beta" >> .env.beta
echo "DJANGO_ENV=beta" >> .env.beta
echo "DB_NAME=shippingservice_beta" >> .env.beta
echo "DB_USER=vignesh" >> .env.beta
echo "DB_PASSWORD=vignesh_secret_pass_2026" >> .env.beta
echo "DB_HOST=16.113.106.152" >> .env.beta
echo "DB_PORT=5432" >> .env.beta
# ── 6. Run Database Migrations ─────────────────────────────────
./venv/bin/python manage.py migrate --database=default --noinput
# ── 7. Configure and restart Systemd service ───────────────────
sudo mv /tmp/shippingservice-beta.service /etc/systemd/system/shippingservice-beta.service
sudo systemctl daemon-reload
sudo systemctl enable shippingservice-beta
sudo systemctl restart shippingservice-beta
# ── 8. Configure CloudWatch Agent ──────────────────────────────
sudo mv /tmp/amazon-cloudwatch-agent.json /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent-shippingservice.json || true
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
-a fetch-config -m ec2 -s \
-c file:/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent-shippingservice.json || true
# ── 9. Configure Nginx Reverse Proxy & SSL Cert ────────────────
sudo mv /tmp/nginx-proxy.conf /etc/nginx/sites-available/shippingservice-beta
sudo ln -sf /etc/nginx/sites-available/shippingservice-beta /etc/nginx/sites-enabled/shippingservice-beta
sudo systemctl restart nginx || true
if [ ! -d "/etc/letsencrypt/live/beta-shipping.tradhox.com" ]; then
sudo certbot certonly --nginx -d beta-shipping.tradhox.com -d shipping.tradhox.com --non-interactive --agree-tos --register-unsafely-without-email || true
fi
sudo systemctl restart nginx
# ── 10. Cleanup tmp files ───────────────────────────────────────
rm -f /tmp/app.tar.gz
EOF

BIN
db.sqlite3 Normal file

Binary file not shown.

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

View 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`.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

22
manage.py Executable file
View file

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shippingservice_project.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

4
requirements.txt Normal file
View file

@ -0,0 +1,4 @@
Django>=5.0,<7.0
requests>=2.31.0
psycopg2-binary>=2.9.0
gunicorn>=21.0.0

0
shipping/__init__.py Normal file
View file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
shipping/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
shipping/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class ShippingConfig(AppConfig):
name = 'shipping'

View file

@ -0,0 +1,35 @@
# Generated by Django 6.1.1 on 2026-09-25 15:42
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='ActivityLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('service_type', models.CharField(choices=[('FORWARD', 'Forward Logistics'), ('REVERSE', 'Reverse Logistics'), ('WEBHOOK', 'Webhook Event')], default='FORWARD', max_length=20)),
('action_name', models.CharField(max_length=100)),
('endpoint_url', models.CharField(max_length=500)),
('http_method', models.CharField(default='POST', max_length=10)),
('request_headers', models.TextField(blank=True, default='')),
('request_body', models.TextField(blank=True, default='')),
('response_status_code', models.IntegerField(default=200)),
('response_body', models.TextField(blank=True, default='')),
('execution_time_ms', models.FloatField(default=0.0)),
('status', models.CharField(choices=[('SUCCESS', 'Success (2xx)'), ('ERROR', 'Error (4xx/5xx)'), ('WARNING', 'Warning')], default='SUCCESS', max_length=20)),
('timestamp', models.DateTimeField(auto_now_add=True)),
('notes', models.TextField(blank=True, default='')),
],
options={
'ordering': ['-timestamp'],
},
),
]

View file

33
shipping/models.py Normal file
View file

@ -0,0 +1,33 @@
from django.db import models
class ActivityLog(models.Model):
SERVICE_CHOICES = [
('FORWARD', 'Forward Logistics'),
('REVERSE', 'Reverse Logistics'),
('WEBHOOK', 'Webhook Event'),
]
STATUS_CHOICES = [
('SUCCESS', 'Success (2xx)'),
('ERROR', 'Error (4xx/5xx)'),
('WARNING', 'Warning'),
]
service_type = models.CharField(max_length=20, choices=SERVICE_CHOICES, default='FORWARD')
action_name = models.CharField(max_length=100)
endpoint_url = models.CharField(max_length=500)
http_method = models.CharField(max_length=10, default='POST')
request_headers = models.TextField(blank=True, default='')
request_body = models.TextField(blank=True, default='')
response_status_code = models.IntegerField(default=200)
response_body = models.TextField(blank=True, default='')
execution_time_ms = models.FloatField(default=0.0)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='SUCCESS')
timestamp = models.DateTimeField(auto_now_add=True)
notes = models.TextField(blank=True, default='')
class Meta:
ordering = ['-timestamp']
def __str__(self):
return f"[{self.service_type}] {self.action_name} - {self.response_status_code} ({self.timestamp.strftime('%H:%M:%S')})"

81
shipping/sfx_client.py Normal file
View file

@ -0,0 +1,81 @@
import json
import time
import requests
from .models import ActivityLog
STAGING_BASE_URL = "https://dale.staging.shadowfax.in"
PRODUCTION_BASE_URL = "https://flash.shadowfax.in"
def get_base_url(env="staging"):
if env == "production":
return PRODUCTION_BASE_URL
return STAGING_BASE_URL
def execute_sfx_call(service_type, action_name, method, full_url, headers, payload=None, env="staging"):
start_time = time.time()
response_status = 500
response_body_str = ""
status_label = "ERROR"
headers_dict = headers or {}
req_body_str = json.dumps(payload, indent=2) if isinstance(payload, (dict, list)) else (payload or "")
req_headers_str = json.dumps(headers_dict, indent=2)
try:
if method.upper() == "GET":
resp = requests.get(full_url, headers=headers_dict, timeout=10)
elif method.upper() == "POST":
resp = requests.post(full_url, headers=headers_dict, json=payload if isinstance(payload, (dict, list)) else None, data=payload if isinstance(payload, str) else None, timeout=10)
elif method.upper() == "PUT":
resp = requests.put(full_url, headers=headers_dict, json=payload if isinstance(payload, (dict, list)) else None, data=payload if isinstance(payload, str) else None, timeout=10)
elif method.upper() == "DELETE":
resp = requests.delete(full_url, headers=headers_dict, timeout=10)
else:
resp = requests.request(method, full_url, headers=headers_dict, json=payload, timeout=10)
response_status = resp.status_code
try:
parsed_json = resp.json()
response_body_str = json.dumps(parsed_json, indent=2)
except Exception:
response_body_str = resp.text
if 200 <= response_status < 300:
status_label = "SUCCESS"
elif 400 <= response_status < 500:
status_label = "ERROR"
else:
status_label = "ERROR"
except requests.exceptions.RequestException as e:
response_status = 502
response_body_str = json.dumps({
"error": "Upstream Connection Failed",
"details": str(e),
"note": "Shadowfax Staging API host may be unreachable directly from your machine or blocked by Akamai WAF. Simulation mode fallback can be enabled."
}, indent=2)
status_label = "ERROR"
duration_ms = round((time.time() - start_time) * 1000, 2)
# Save to ActivityLog DB model
log_entry = ActivityLog.objects.create(
service_type=service_type,
action_name=action_name,
endpoint_url=full_url,
http_method=method.upper(),
request_headers=req_headers_str,
request_body=req_body_str,
response_status_code=response_status,
response_body=response_body_str,
execution_time_ms=duration_ms,
status=status_label
)
return {
"log_id": log_entry.id,
"status_code": response_status,
"execution_time_ms": duration_ms,
"status": status_label,
"response": response_body_str
}

View file

@ -0,0 +1,294 @@
:root {
--bg-dark: #0f172a;
--bg-card: #1e293b;
--bg-input: #0f172a;
--border-color: #334155;
--text-main: #f8fafc;
--text-muted: #94a3b8;
--accent-cyan: #06b6d4;
--accent-blue: #3b82f6;
--accent-purple: #8b5cf6;
--accent-pink: #ec4899;
--accent-green: #10b981;
--accent-amber: #f59e0b;
--accent-red: #ef4444;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background-color: var(--bg-dark);
color: var(--text-main);
line-height: 1.5;
min-height: 100vh;
}
/* Navbar */
.navbar {
background: rgba(30, 41, 59, 0.8);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border-color);
padding: 1rem 2rem;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 100;
}
.brand {
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 700;
font-size: 1.25rem;
background: linear-gradient(135deg, var(--accent-cyan), var(--accent-blue));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.nav-tabs {
display: flex;
gap: 0.5rem;
}
.nav-link {
color: var(--text-muted);
text-decoration: none;
padding: 0.5rem 1.25rem;
border-radius: 0.5rem;
font-weight: 5rem;
font-size: 0.9rem;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.5rem;
}
.nav-link:hover {
color: var(--text-main);
background: rgba(255, 255, 255, 0.05);
}
.nav-link.active {
color: #fff;
background: linear-gradient(135deg, rgba(6, 182, 212, 0.2), rgba(59, 130, 246, 0.2));
border: 1px solid rgba(6, 182, 212, 0.4);
}
/* Layout Container */
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.page-header {
margin-bottom: 2rem;
}
.page-title {
font-size: 1.75rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.page-desc {
color: var(--text-muted);
font-size: 0.95rem;
}
/* Grid & Cards */
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
@media (max-width: 1024px) {
.grid-2 {
grid-template-columns: 1fr;
}
}
.card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 0.75rem;
padding: 1.5rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.card-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border-color);
padding-bottom: 0.75rem;
}
/* Form Controls */
.form-group {
margin-bottom: 1rem;
}
.form-label {
display: block;
font-size: 0.85rem;
font-weight: 500;
color: var(--text-muted);
margin-bottom: 0.35rem;
}
.form-control, .form-select {
width: 100%;
background: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: 0.5rem;
padding: 0.6rem 0.85rem;
color: var(--text-main);
font-size: 0.9rem;
outline: none;
transition: border-color 0.2s;
}
.form-control:focus, .form-select:focus {
border-color: var(--accent-cyan);
box-shadow: 0 0 0 2px rgba(6, 182, 212, 0.2);
}
textarea.form-control {
font-family: 'Fira Code', monospace;
font-size: 0.85rem;
line-height: 1.4;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.65rem 1.25rem;
font-size: 0.9rem;
font-weight: 600;
border-radius: 0.5rem;
border: none;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: linear-gradient(135deg, var(--accent-cyan), var(--accent-blue));
color: #fff;
}
.btn-primary:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.btn-secondary {
background: #334155;
color: #fff;
}
.btn-secondary:hover {
background: #475569;
}
.btn-danger {
background: var(--accent-red);
color: #fff;
}
/* JSON Output Inspector */
.json-viewer {
background: #090d16;
border: 1px solid var(--border-color);
border-radius: 0.5rem;
padding: 1rem;
font-family: 'Fira Code', 'Courier New', monospace;
font-size: 0.85rem;
color: #38bdf8;
max-height: 400px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
/* Table */
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
}
.data-table th {
background: rgba(15, 23, 42, 0.6);
color: var(--text-muted);
font-weight: 600;
text-align: left;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border-color);
}
.data-table td {
padding: 0.75rem 1rem;
border-bottom: 1px solid rgba(51, 65, 85, 0.4);
}
.data-table tr:hover {
background: rgba(255, 255, 255, 0.02);
}
/* Badges */
.badge {
display: inline-block;
padding: 0.2rem 0.55rem;
border-radius: 0.35rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.badge-success { background: rgba(16, 185, 129, 0.2); color: #34d399; border: 1px solid rgba(16, 185, 129, 0.4); }
.badge-error { background: rgba(239, 68, 68, 0.2); color: #f87171; border: 1px solid rgba(239, 68, 68, 0.4); }
.badge-forward { background: rgba(59, 130, 246, 0.2); color: #60a5fa; border: 1px solid rgba(59, 130, 246, 0.4); }
.badge-reverse { background: rgba(236, 72, 153, 0.2); color: #f472b6; border: 1px solid rgba(236, 72, 153, 0.4); }
.badge-webhook { background: rgba(139, 92, 246, 0.2); color: #a78bfa; border: 1px solid rgba(139, 92, 246, 0.4); }
/* Modal */
.modal {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(4px);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 0.75rem;
width: 90%;
max-width: 800px;
max-height: 90vh;
overflow-y: auto;
padding: 1.5rem;
}

View file

@ -0,0 +1,44 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Shadowfax (SFX) Shipping Platform{% endblock %}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{% static 'css/style.css' %}">
{% block extra_head %}{% endblock %}
</head>
<body>
<!-- Navigation Header -->
<nav class="navbar">
<div class="brand">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
Shadowfax (SFX) API Testing Suite
</div>
<div class="nav-tabs">
<a href="{% url 'forward_view' %}" class="nav-link {% if request.resolver_match.url_name == 'forward_view' %}active{% endif %}">
📦 Forward Pickup & Delivery
</a>
<a href="{% url 'reverse_view' %}" class="nav-link {% if request.resolver_match.url_name == 'reverse_view' %}active{% endif %}">
🔄 Reverse Pickup (QC)
</a>
<a href="{% url 'logs_view' %}" class="nav-link {% if request.resolver_match.url_name == 'logs_view' %}active{% endif %}">
📋 Activity Audit Logs
</a>
<a href="{% url 'webhooks_ui_view' %}" class="nav-link {% if request.resolver_match.url_name == 'webhooks_ui_view' %}active{% endif %}">
⚡ Webhook Listener
</a>
</div>
</nav>
<!-- Main Body Content -->
<div class="container">
{% block content %}{% endblock %}
</div>
{% block extra_scripts %}{% endblock %}
</body>
</html>

View file

@ -0,0 +1,330 @@
{% extends 'shipping/base.html' %}
{% block title %}Forward Pickup & Delivery — SFX API Hub{% endblock %}
{% block content %}
<div class="page-header">
<h1 class="page-title">📦 Forward Pickup & Delivery Hub</h1>
<p class="page-desc">Test seller pickups, warehouse fulfillment, AWB generation, order tracking, cancellations, and POD retrieval for Shadowfax Forward Logistics.</p>
</div>
<!-- Global Config Card -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title">
<span>🌐 API Configuration & Environment Setup</span>
<span class="badge badge-forward">Forward Logistics API</span>
</div>
<div style="display: grid; grid-template-columns: 1fr 2fr 1fr auto; gap: 1rem; align-items: end;">
<div class="form-group" style="margin: 0;">
<label class="form-label">Target Environment</label>
<select id="cfg_env" class="form-select">
<option value="staging" selected>Staging (dale.staging.shadowfax.in)</option>
<option value="production">Production (flash.shadowfax.in)</option>
</select>
</div>
<div class="form-group" style="margin: 0;">
<label class="form-label">Shadowfax Authorization API Token</label>
<input type="text" id="cfg_token" class="form-control" value="sample_staging_token_12345">
</div>
<div class="form-group" style="margin: 0;">
<label class="form-label">Execution Mode</label>
<select id="cfg_mock" class="form-select">
<option value="true" selected>Simulation Mode (Recommended for QA)</option>
<option value="false">Live Upstream Call</option>
</select>
</div>
</div>
</div>
<div class="grid-2">
<!-- Left Column: Interactive Forms -->
<div>
<!-- Feature 1: Serviceability Check -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title">1. Pincode Serviceability Check</div>
<form id="form_serviceability" onsubmit="executeAction(event, 'serviceability')">
<div class="form-group">
<label class="form-label">Pincodes (Comma Separated)</label>
<input type="text" name="pincodes" class="form-control" value="560077,110009">
</div>
<div class="form-group">
<label class="form-label">Service Type</label>
<select name="service" class="form-select">
<option value="express" selected>Express Delivery</option>
<option value="surface">Surface Delivery</option>
</select>
</div>
<button type="submit" class="btn btn-primary">Check Serviceability</button>
</form>
</div>
<!-- Feature 2: Generate AWB -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title">2. Pre-allocate AWB Numbers</div>
<form id="form_generate_awb" onsubmit="executeAction(event, 'generate_awb')">
<div class="grid-2" style="gap: 0.75rem;">
<div class="form-group">
<label class="form-label">Client Code</label>
<input type="text" name="client_code" class="form-control" value="ACME_RETAIL">
</div>
<div class="form-group">
<label class="form-label">Count</label>
<input type="number" name="count" class="form-control" value="1" min="1" max="100">
</div>
</div>
<button type="submit" class="btn btn-primary">Generate AWB</button>
</form>
</div>
<!-- Feature 3: Create Marketplace Order -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title">3. Create Marketplace Order (Seller Pickup)</div>
<form id="form_create_mkt" onsubmit="executeRawAction(event, 'create_marketplace_order', 'payload_mkt')">
<div class="form-group">
<label class="form-label">Order Payload (JSON)</label>
<textarea id="payload_mkt" name="payload" class="form-control" rows="12">{
"order_type": "marketplace",
"order_details": {
"client_order_id": "ORD-MKT-2026-101",
"actual_weight": 750.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",
"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
}
]
}</textarea>
</div>
<button type="submit" class="btn btn-primary">Create Marketplace Order</button>
</form>
</div>
<!-- Feature 4: Create Warehouse Order -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title">4. Create Warehouse Order (Hub Dispatch)</div>
<form id="form_create_wh" onsubmit="executeRawAction(event, 'create_warehouse_order', 'payload_wh')">
<div class="form-group">
<label class="form-label">Warehouse Order Payload (JSON)</label>
<textarea id="payload_wh" name="payload" class="form-control" rows="10">{
"order_type": "warehouse",
"order_details": {
"client_order_id": "ORD-WH-2026-505",
"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
}
]
}</textarea>
</div>
<button type="submit" class="btn btn-primary">Create Warehouse Order</button>
</form>
</div>
</div>
<!-- Right Column: Tracking, Utilities & Response Inspector -->
<div>
<!-- Response Inspector Card -->
<div class="card" style="margin-bottom: 1.5rem; border-color: var(--accent-cyan);">
<div class="card-title">
<span>⚡ Live API Response & Inspector</span>
<span id="res_status_badge" class="badge badge-success">Awaiting Request</span>
</div>
<div style="font-size: 0.8rem; color: var(--text-muted); margin-bottom: 0.5rem; display: flex; justify-content: space-between;">
<span id="res_time">Execution Time: - ms</span>
<span id="res_code">HTTP Status: -</span>
</div>
<div id="response_inspector" class="json-viewer">// Select an action on the left to execute API call and inspect response payload...</div>
</div>
<!-- Feature 5: Track Single Order -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title">5. Track Single Order V4</div>
<form id="form_track_single" onsubmit="executeAction(event, 'track_single')">
<div class="form-group">
<label class="form-label">AWB Number</label>
<input type="text" name="awb_number" class="form-control" value="SFX9928374102">
</div>
<button type="submit" class="btn btn-primary">Track Order</button>
</form>
</div>
<!-- Feature 6: Bulk Track Orders -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title">6. Track Bulk Orders V4</div>
<form id="form_track_bulk" onsubmit="executeAction(event, 'track_bulk')">
<div class="form-group">
<label class="form-label">AWB Numbers (Comma Separated)</label>
<input type="text" name="awb_numbers" class="form-control" value="SFX9928374102,SFX8817263541">
</div>
<button type="submit" class="btn btn-primary">Bulk Track</button>
</form>
</div>
<!-- Feature 7: Cancel Order -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title">7. Cancel Order</div>
<form id="form_cancel" onsubmit="executeAction(event, 'cancel_order')">
<div class="grid-2" style="gap: 0.75rem;">
<div class="form-group">
<label class="form-label">Client Order ID</label>
<input type="text" name="client_order_id" class="form-control" value="ORD-MKT-2026-101">
</div>
<div class="form-group">
<label class="form-label">AWB Number</label>
<input type="text" name="awb_number" class="form-control" value="SFX9928374102">
</div>
</div>
<div class="form-group">
<label class="form-label">Cancellation Reason</label>
<input type="text" name="reason" class="form-control" value="Customer requested cancellation">
</div>
<button type="submit" class="btn btn-danger">Cancel Order</button>
</form>
</div>
<!-- Feature 8: Dynamic QR Code -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title">8. Generate Dynamic UPI QR Code V2</div>
<form id="form_generate_qr" onsubmit="executeAction(event, 'generate_qr')">
<div class="grid-2" style="gap: 0.75rem;">
<div class="form-group">
<label class="form-label">AWB Number</label>
<input type="text" name="awb_number" class="form-control" value="SFX9928374102">
</div>
<div class="form-group">
<label class="form-label">COD Amount (INR)</label>
<input type="number" step="0.01" name="cod_amount" class="form-control" value="1499.00">
</div>
</div>
<button type="submit" class="btn btn-primary">Generate QR Code</button>
</form>
</div>
<!-- Feature 9: Proof of Delivery (POD) -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title">9. Get POD Details</div>
<form id="form_pod" onsubmit="executeAction(event, 'get_pod')">
<div class="form-group">
<label class="form-label">AWB Number</label>
<input type="text" name="awb_number" class="form-control" value="SFX9928374102">
</div>
<button type="submit" class="btn btn-secondary">Fetch POD Signature/Photos</button>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
function executeAction(e, actionType) {
e.preventDefault();
const form = e.target;
const formData = new FormData(form);
formData.append('action_type', actionType);
formData.append('environment', document.getElementById('cfg_env').value);
formData.append('api_token', document.getElementById('cfg_token').value);
formData.append('use_mock', document.getElementById('cfg_mock').value);
sendApiRequest(formData);
}
function executeRawAction(e, actionType, textareaId) {
e.preventDefault();
const formData = new FormData();
formData.append('action_type', actionType);
formData.append('payload', document.getElementById(textareaId).value);
formData.append('environment', document.getElementById('cfg_env').value);
formData.append('api_token', document.getElementById('cfg_token').value);
formData.append('use_mock', document.getElementById('cfg_mock').value);
sendApiRequest(formData);
}
function sendApiRequest(formData) {
const inspector = document.getElementById('response_inspector');
inspector.textContent = "// Executing API request...";
fetch("{% url 'forward_view' %}", {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': '{{ csrf_token }}'
}
})
.then(resp => resp.json())
.then(data => {
document.getElementById('res_time').textContent = `Execution Time: ${data.execution_time_ms || 0} ms`;
document.getElementById('res_code').textContent = `HTTP Status: ${data.status_code || 200}`;
const badge = document.getElementById('res_status_badge');
badge.textContent = data.status || 'SUCCESS';
badge.className = data.status === 'SUCCESS' ? 'badge badge-success' : 'badge badge-error';
try {
const formatted = typeof data.response === 'string' ? JSON.stringify(JSON.parse(data.response), null, 2) : JSON.stringify(data.response, null, 2);
inspector.textContent = formatted;
} catch(err) {
inspector.textContent = data.response;
}
})
.catch(err => {
inspector.textContent = `Client Network Error: ${err}`;
});
}
</script>
{% endblock %}

View file

@ -0,0 +1,183 @@
{% extends 'shipping/base.html' %}
{% block title %}Activity Audit Logs — SFX API Hub{% endblock %}
{% block content %}
<div class="page-header">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<h1 class="page-title">📋 Activity Audit Logs</h1>
<p class="page-desc">Complete audit trail capturing all API calls, HTTP headers, request payloads, response codes, execution times, and errors across Forward and Reverse operations.</p>
</div>
<form action="{% url 'clear_logs_view' %}" method="POST" onsubmit="return confirm('Are you sure you want to clear all audit logs?');">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Clear All Logs</button>
</form>
</div>
</div>
<!-- Filters Bar Card -->
<div class="card" style="margin-bottom: 1.5rem;">
<form method="GET" action="{% url 'logs_view' %}" style="display: grid; grid-template-columns: 2fr 1fr 1fr auto; gap: 1rem; align-items: end;">
<div class="form-group" style="margin: 0;">
<label class="form-label">Search Query (Order ID, AWB, Endpoint)</label>
<input type="text" name="q" class="form-control" placeholder="Search by AWB, Order ID, or keyword..." value="{{ query }}">
</div>
<div class="form-group" style="margin: 0;">
<label class="form-label">Service Filter</label>
<select name="service" class="form-select">
<option value="">All Services</option>
<option value="FORWARD" {% if service_filter == 'FORWARD' %}selected{% endif %}>Forward Logistics</option>
<option value="REVERSE" {% if service_filter == 'REVERSE' %}selected{% endif %}>Reverse Logistics</option>
<option value="WEBHOOK" {% if service_filter == 'WEBHOOK' %}selected{% endif %}>Webhook Events</option>
</select>
</div>
<div class="form-group" style="margin: 0;">
<label class="form-label">Status Filter</label>
<select name="status" class="form-select">
<option value="">All Statuses</option>
<option value="SUCCESS" {% if status_filter == 'SUCCESS' %}selected{% endif %}>Success (2xx)</option>
<option value="ERROR" {% if status_filter == 'ERROR' %}selected{% endif %}>Error (4xx/5xx)</option>
</select>
</div>
<div>
<button type="submit" class="btn btn-primary">Filter Logs</button>
</div>
</form>
</div>
<!-- Logs Data Table Card -->
<div class="card">
<div class="card-title">
<span>Audit Log History (Showing Latest {{ logs|length }} Entries)</span>
</div>
<table class="data-table">
<thead>
<tr>
<th>Timestamp</th>
<th>Service</th>
<th>Action Name</th>
<th>Method & Endpoint</th>
<th>Status Code</th>
<th>Latency</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr>
<td style="font-family: 'Fira Code', monospace; font-size: 0.8rem; color: var(--text-muted);">
{{ log.timestamp|date:"Y-m-d H:i:s" }}
</td>
<td>
{% if log.service_type == 'FORWARD' %}
<span class="badge badge-forward">Forward</span>
{% elif log.service_type == 'REVERSE' %}
<span class="badge badge-reverse">Reverse</span>
{% else %}
<span class="badge badge-webhook">Webhook</span>
{% endif %}
</td>
<td style="font-weight: 600;">{{ log.action_name }}</td>
<td>
<span style="font-family: 'Fira Code', monospace; font-size: 0.8rem; color: var(--accent-cyan); font-weight: 600;">{{ log.http_method }}</span>
<span style="font-family: 'Fira Code', monospace; font-size: 0.8rem; color: var(--text-muted);">{{ log.endpoint_url }}</span>
</td>
<td>
{% if log.response_status_code >= 200 and log.response_status_code < 300 %}
<span class="badge badge-success">{{ log.response_status_code }} OK</span>
{% else %}
<span class="badge badge-error">{{ log.response_status_code }} Error</span>
{% endif %}
</td>
<td style="font-family: 'Fira Code', monospace; font-size: 0.8rem;">
{{ log.execution_time_ms }} ms
</td>
<td>
<button class="btn btn-secondary" style="padding: 0.35rem 0.75rem; font-size: 0.8rem;" onclick="openInspectorModal({{ log.id }})">
Inspect Log
</button>
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" style="text-align: center; color: var(--text-muted); padding: 2rem;">
No audit logs recorded matching criteria. Execute API calls from Forward or Reverse hubs to populate logs.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Log Detail Inspector Modal -->
<div id="inspector_modal" class="modal">
<div class="modal-content">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; border-bottom: 1px solid var(--border-color); padding-bottom: 0.75rem;">
<h3 style="font-size: 1.1rem; font-weight: 600;" id="modal_title">Inspect Log Entry #</h3>
<button onclick="closeInspectorModal()" style="background: none; border: none; color: var(--text-muted); font-size: 1.5rem; cursor: pointer;">&times;</button>
</div>
<div style="margin-bottom: 1rem;">
<div style="font-size: 0.85rem; color: var(--text-muted); margin-bottom: 0.25rem;">Endpoint URL & Method</div>
<div id="modal_url" style="font-family: 'Fira Code', monospace; font-size: 0.85rem; color: var(--accent-cyan); background: var(--bg-input); padding: 0.5rem; border-radius: 0.35rem;">-</div>
</div>
<div style="margin-bottom: 1rem;">
<div style="font-size: 0.85rem; color: var(--text-muted); margin-bottom: 0.25rem;">Request Headers</div>
<pre id="modal_req_headers" class="json-viewer" style="max-height: 150px;">-</pre>
</div>
<div style="margin-bottom: 1rem;">
<div style="font-size: 0.85rem; color: var(--text-muted); margin-bottom: 0.25rem;">Request Payload</div>
<pre id="modal_req_body" class="json-viewer" style="max-height: 200px;">-</pre>
</div>
<div style="margin-bottom: 1rem;">
<div style="font-size: 0.85rem; color: var(--text-muted); margin-bottom: 0.25rem;">Response Payload</div>
<pre id="modal_res_body" class="json-viewer" style="max-height: 250px;">-</pre>
</div>
<div style="text-align: right;">
<button class="btn btn-secondary" onclick="closeInspectorModal()">Close Inspector</button>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
function openInspectorModal(logId) {
fetch(`/logs/${logId}/`)
.then(resp => resp.json())
.then(data => {
document.getElementById('modal_title').textContent = `Inspect Log #${data.id} — ${data.action_name} (${data.service_type})`;
document.getElementById('modal_url').textContent = `${data.http_method} ${data.endpoint_url}`;
try {
document.getElementById('modal_req_headers').textContent = JSON.stringify(JSON.parse(data.request_headers), null, 2);
} catch(e) {
document.getElementById('modal_req_headers').textContent = data.request_headers || 'No headers recorded';
}
try {
document.getElementById('modal_req_body').textContent = JSON.stringify(JSON.parse(data.request_body), null, 2);
} catch(e) {
document.getElementById('modal_req_body').textContent = data.request_body || 'No request body payload';
}
try {
document.getElementById('modal_res_body').textContent = JSON.stringify(JSON.parse(data.response_body), null, 2);
} catch(e) {
document.getElementById('modal_res_body').textContent = data.response_body || 'No response body payload';
}
document.getElementById('inspector_modal').classList.add('active');
});
}
function closeInspectorModal() {
document.getElementById('inspector_modal').classList.remove('active');
}
</script>
{% endblock %}

View file

@ -0,0 +1,266 @@
{% extends 'shipping/base.html' %}
{% block title %}Reverse Pickup (QC) — SFX API Hub{% endblock %}
{% block content %}
<div class="page-header">
<h1 class="page-title">🔄 Reverse Pickup & Doorstep Quality Check (QC) Hub</h1>
<p class="page-desc">Test customer doorstep return pickups, Quality Check (QC) enforcement rules, brand/serial validation, reverse tracking, and warehouse return handovers.</p>
</div>
<!-- Global Config Card -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title">
<span>🌐 API Configuration & Environment Setup</span>
<span class="badge badge-reverse">Reverse Logistics API</span>
</div>
<div style="display: grid; grid-template-columns: 1fr 2fr 1fr auto; gap: 1rem; align-items: end;">
<div class="form-group" style="margin: 0;">
<label class="form-label">Target Environment</label>
<select id="cfg_env" class="form-select">
<option value="staging" selected>Staging (dale.staging.shadowfax.in)</option>
<option value="production">Production (flash.shadowfax.in)</option>
</select>
</div>
<div class="form-group" style="margin: 0;">
<label class="form-label">Shadowfax Authorization API Token</label>
<input type="text" id="cfg_token" class="form-control" value="sample_staging_token_12345">
</div>
<div class="form-group" style="margin: 0;">
<label class="form-label">Execution Mode</label>
<select id="cfg_mock" class="form-select">
<option value="true" selected>Simulation Mode (Recommended for QA)</option>
<option value="false">Live Upstream Call</option>
</select>
</div>
</div>
</div>
<div class="grid-2">
<!-- Left Column: Interactive Forms -->
<div>
<!-- Feature 1: Reverse Serviceability Check -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title">1. Reverse Pincode Serviceability Check</div>
<form id="form_reverse_serviceability" onsubmit="executeAction(event, 'reverse_serviceability')">
<div class="form-group">
<label class="form-label">Customer Pickup Pincodes (Comma Separated)</label>
<input type="text" name="pincodes" class="form-control" value="560077,110009">
</div>
<button type="submit" class="btn btn-primary">Check Reverse Serviceability</button>
</form>
</div>
<!-- Feature 2: Generate Reverse AWB -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title">2. Pre-allocate Reverse AWB Numbers</div>
<form id="form_generate_reverse_awb" onsubmit="executeAction(event, 'generate_reverse_awb')">
<div class="grid-2" style="gap: 0.75rem;">
<div class="form-group">
<label class="form-label">Client Code</label>
<input type="text" name="client_code" class="form-control" value="ACME_RETAIL">
</div>
<div class="form-group">
<label class="form-label">Count</label>
<input type="number" name="count" class="form-control" value="1" min="1" max="100">
</div>
</div>
<button type="submit" class="btn btn-primary">Generate Reverse AWB</button>
</form>
</div>
<!-- Feature 3: Create Reverse Pickup Request -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title">3. Create Reverse Pickup Request (Doorstep QC)</div>
<form id="form_create_reverse" onsubmit="executeRawAction(event, 'create_reverse_request', 'payload_reverse')">
<div class="form-group">
<label class="form-label">Reverse Request Payload with QC Rules (JSON)</label>
<textarea id="payload_reverse" name="payload" class="form-control" rows="14">{
"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
}
}</textarea>
</div>
<button type="submit" class="btn btn-primary">Create Reverse Pickup Request</button>
</form>
</div>
<!-- Feature 4: Update QC Flag -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title">4. Update QC Flags & Serial Number</div>
<form id="form_update_qc" onsubmit="executeAction(event, 'update_qc')">
<div class="form-group">
<label class="form-label">Client Request ID</label>
<input type="text" name="client_request_id" class="form-control" value="REV-ORD-2026-8801">
</div>
<div class="grid-2" style="gap: 0.75rem;">
<div class="form-group">
<label class="form-label">Enforce Doorstep QC?</label>
<select name="qc_check" class="form-select">
<option value="true" selected>True (Mandatory QC)</option>
<option value="false">False (Bypass QC)</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Updated Serial / IMEI</label>
<input type="text" name="serial_number" class="form-control" value="SN-9988112244">
</div>
</div>
<button type="submit" class="btn btn-secondary">Update QC Flags</button>
</form>
</div>
</div>
<!-- Right Column: Reverse Tracking & Inspector -->
<div>
<!-- Response Inspector Card -->
<div class="card" style="margin-bottom: 1.5rem; border-color: var(--accent-pink);">
<div class="card-title">
<span>⚡ Live Reverse Response Inspector</span>
<span id="res_status_badge" class="badge badge-success">Awaiting Request</span>
</div>
<div style="font-size: 0.8rem; color: var(--text-muted); margin-bottom: 0.5rem; display: flex; justify-content: space-between;">
<span id="res_time">Execution Time: - ms</span>
<span id="res_code">HTTP Status: -</span>
</div>
<div id="response_inspector" class="json-viewer">// Select a reverse action on the left to test API call and inspect response payload...</div>
</div>
<!-- Feature 5: Track Single Reverse Pickup -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title">5. Track Single Reverse Pickup V4</div>
<form id="form_track_reverse_single" onsubmit="executeAction(event, 'track_reverse_single')">
<div class="form-group">
<label class="form-label">Client Request ID</label>
<input type="text" name="client_request_id" class="form-control" value="REV-ORD-2026-8801">
</div>
<button type="submit" class="btn btn-primary">Track Reverse Request</button>
</form>
</div>
<!-- Feature 6: Bulk Track Reverse Pickups -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title">6. Bulk Track Reverse Pickups V4</div>
<form id="form_track_reverse_bulk" onsubmit="executeAction(event, 'track_reverse_bulk')">
<div class="form-group">
<label class="form-label">Client Request IDs (Comma Separated)</label>
<input type="text" name="client_request_ids" class="form-control" value="REV-ORD-2026-8801,REV-ORD-2026-8802">
</div>
<button type="submit" class="btn btn-primary">Bulk Track Reverse Requests</button>
</form>
</div>
<!-- Feature 7: Cancel Reverse Request -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title">7. Cancel Reverse Pickup Request</div>
<form id="form_cancel_reverse" onsubmit="executeAction(event, 'cancel_reverse')">
<div class="form-group">
<label class="form-label">Client Request ID</label>
<input type="text" name="client_request_id" class="form-control" value="REV-ORD-2026-8801">
</div>
<div class="form-group">
<label class="form-label">Cancellation Reason</label>
<input type="text" name="reason" class="form-control" value="Customer withdrew return request">
</div>
<button type="submit" class="btn btn-danger">Cancel Reverse Pickup</button>
</form>
</div>
<!-- Feature 8: Proof of Pickup (POP) -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-title">8. Get Proof of Pickup (POP) & QC Photos</div>
<form id="form_reverse_pod" onsubmit="executeAction(event, 'get_reverse_pod')">
<div class="form-group">
<label class="form-label">Client Request ID</label>
<input type="text" name="client_request_id" class="form-control" value="REV-ORD-2026-8801">
</div>
<button type="submit" class="btn btn-secondary">Fetch POP & Doorstep QC Photos</button>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
function executeAction(e, actionType) {
e.preventDefault();
const form = e.target;
const formData = new FormData(form);
formData.append('action_type', actionType);
formData.append('environment', document.getElementById('cfg_env').value);
formData.append('api_token', document.getElementById('cfg_token').value);
formData.append('use_mock', document.getElementById('cfg_mock').value);
sendApiRequest(formData);
}
function executeRawAction(e, actionType, textareaId) {
e.preventDefault();
const formData = new FormData();
formData.append('action_type', actionType);
formData.append('payload', document.getElementById(textareaId).value);
formData.append('environment', document.getElementById('cfg_env').value);
formData.append('api_token', document.getElementById('cfg_token').value);
formData.append('use_mock', document.getElementById('cfg_mock').value);
sendApiRequest(formData);
}
function sendApiRequest(formData) {
const inspector = document.getElementById('response_inspector');
inspector.textContent = "// Executing Reverse API request...";
fetch("{% url 'reverse_view' %}", {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': '{{ csrf_token }}'
}
})
.then(resp => resp.json())
.then(data => {
document.getElementById('res_time').textContent = `Execution Time: ${data.execution_time_ms || 0} ms`;
document.getElementById('res_code').textContent = `HTTP Status: ${data.status_code || 200}`;
const badge = document.getElementById('res_status_badge');
badge.textContent = data.status || 'SUCCESS';
badge.className = data.status === 'SUCCESS' ? 'badge badge-success' : 'badge badge-error';
try {
const formatted = typeof data.response === 'string' ? JSON.stringify(JSON.parse(data.response), null, 2) : JSON.stringify(data.response, null, 2);
inspector.textContent = formatted;
} catch(err) {
inspector.textContent = data.response;
}
})
.catch(err => {
inspector.textContent = `Client Network Error: ${err}`;
});
}
</script>
{% endblock %}

View file

@ -0,0 +1,176 @@
{% extends 'shipping/base.html' %}
{% block title %}Webhook Listener & Simulator — SFX API Hub{% endblock %}
{% block content %}
<div class="page-header">
<h1 class="page-title">⚡ Webhook Listener & Callback Event Simulator</h1>
<p class="page-desc">Monitor real-time push notifications sent by Shadowfax when shipments transition state. Use the simulator to test your integration without waiting for physical delivery riders.</p>
</div>
<!-- Webhook URL Info & Simulator Card -->
<div class="grid-2" style="margin-bottom: 1.5rem;">
<div class="card">
<div class="card-title">
<span>📡 Live Webhook Listener Endpoint</span>
<span class="badge badge-success">Active</span>
</div>
<div class="form-group">
<label class="form-label">Client Callback Endpoint URL</label>
<input type="text" class="form-control" value="http://localhost:8000/api/webhook/" readonly style="font-family: 'Fira Code', monospace; color: var(--accent-cyan);">
</div>
<p style="font-size: 0.85rem; color: var(--text-muted);">Configure this endpoint in your Shadowfax Merchant Portal to receive HTTP POST JSON notifications for shipment status updates and doorstep Quality Check results.</p>
</div>
<div class="card">
<div class="card-title">
<span>🧪 Trigger Mock Webhook Callback</span>
<span class="badge badge-webhook">Simulator Tool</span>
</div>
<form method="POST" action="{% url 'webhooks_ui_view' %}">
{% csrf_token %}
<div class="grid-2" style="gap: 0.75rem;">
<div class="form-group">
<label class="form-label">Service Type</label>
<select name="service_type" class="form-select">
<option value="FORWARD" selected>Forward Order Callback</option>
<option value="REVERSE">Reverse Pickup Callback</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Event / State Transition</label>
<select name="event_type" class="form-select">
<option value="pickup_assigned">pickup_assigned</option>
<option value="picked_up">picked_up (QC Passed)</option>
<option value="qc_failed">qc_failed (QC Rejected)</option>
<option value="in_transit">in_transit</option>
<option value="out_for_delivery" selected>out_for_delivery</option>
<option value="delivered">delivered</option>
<option value="delivery_rescheduled">delivery_rescheduled</option>
<option value="rts_initiated">rts_initiated</option>
<option value="rto_delivered">rto_delivered_to_warehouse</option>
</select>
</div>
</div>
<div class="grid-2" style="gap: 0.75rem;">
<div class="form-group">
<label class="form-label">Client Order / Request ID</label>
<input type="text" name="order_id" class="form-control" value="ORD-MKT-2026-101">
</div>
<div class="form-group">
<label class="form-label">AWB Number</label>
<input type="text" name="awb_number" class="form-control" value="SFX9928374102">
</div>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%;">Trigger Simulated Callback Event</button>
</form>
</div>
</div>
<!-- Webhook Logs Table -->
<div class="card">
<div class="card-title">
<span>Received Webhook Events History ({{ webhook_logs|length }} Events)</span>
</div>
<table class="data-table">
<thead>
<tr>
<th>Timestamp</th>
<th>Event Name</th>
<th>Endpoint</th>
<th>HTTP Status</th>
<th>Received Payload Preview</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for log in webhook_logs %}
<tr>
<td style="font-family: 'Fira Code', monospace; font-size: 0.8rem; color: var(--text-muted);">
{{ log.timestamp|date:"Y-m-d H:i:s" }}
</td>
<td>
<span class="badge badge-webhook">{{ log.action_name }}</span>
</td>
<td style="font-family: 'Fira Code', monospace; font-size: 0.8rem;">
{{ log.endpoint_url }}
</td>
<td>
<span class="badge badge-success">{{ log.response_status_code }} OK</span>
</td>
<td style="font-family: 'Fira Code', monospace; font-size: 0.8rem; max-width: 350px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
{{ log.request_body }}
</td>
<td>
<button class="btn btn-secondary" style="padding: 0.35rem 0.75rem; font-size: 0.8rem;" onclick="openInspectorModal({{ log.id }})">
View Payload
</button>
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" style="text-align: center; color: var(--text-muted); padding: 2rem;">
No webhook callback events received yet. Use the Simulator Tool above or send a POST request to <code>http://localhost:8000/api/webhook/</code>.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Modal for Viewing Payload -->
<div id="inspector_modal" class="modal">
<div class="modal-content">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; border-bottom: 1px solid var(--border-color); padding-bottom: 0.75rem;">
<h3 style="font-size: 1.1rem; font-weight: 600;" id="modal_title">Inspect Webhook Callback</h3>
<button onclick="closeInspectorModal()" style="background: none; border: none; color: var(--text-muted); font-size: 1.5rem; cursor: pointer;">&times;</button>
</div>
<div style="margin-bottom: 1rem;">
<div style="font-size: 0.85rem; color: var(--text-muted); margin-bottom: 0.25rem;">Webhook HTTP Headers</div>
<pre id="modal_req_headers" class="json-viewer" style="max-height: 150px;">-</pre>
</div>
<div style="margin-bottom: 1rem;">
<div style="font-size: 0.85rem; color: var(--text-muted); margin-bottom: 0.25rem;">Callback Payload (JSON)</div>
<pre id="modal_req_body" class="json-viewer" style="max-height: 250px;">-</pre>
</div>
<div style="text-align: right;">
<button class="btn btn-secondary" onclick="closeInspectorModal()">Close</button>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
function openInspectorModal(logId) {
fetch(`/logs/${logId}/`)
.then(resp => resp.json())
.then(data => {
document.getElementById('modal_title').textContent = `Inspect Webhook #${data.id} — ${data.action_name}`;
try {
document.getElementById('modal_req_headers').textContent = JSON.stringify(JSON.parse(data.request_headers), null, 2);
} catch(e) {
document.getElementById('modal_req_headers').textContent = data.request_headers || 'No headers';
}
try {
document.getElementById('modal_req_body').textContent = JSON.stringify(JSON.parse(data.request_body), null, 2);
} catch(e) {
document.getElementById('modal_req_body').textContent = data.request_body || 'No payload';
}
document.getElementById('inspector_modal').classList.add('active');
});
}
function closeInspectorModal() {
document.getElementById('inspector_modal').classList.remove('active');
}
</script>
{% endblock %}

3
shipping/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

20
shipping/urls.py Normal file
View file

@ -0,0 +1,20 @@
from django.urls import path
from . import views
urlpatterns = [
# Dashboard Web UI Routes
path('', views.index_redirect, name='index_redirect'),
path('forward/', views.forward_view, name='forward_view'),
path('reverse/', views.reverse_view, name='reverse_view'),
path('logs/', views.logs_view, name='logs_view'),
path('logs/<int:log_id>/', views.log_detail_api, name='log_detail_api'),
path('logs/clear/', views.clear_logs_view, name='clear_logs_view'),
path('webhooks/', views.webhooks_ui_view, name='webhooks_ui_view'),
path('api/webhook/', views.webhook_listener_api, name='webhook_listener_api'),
# Microservices REST API Endpoints under /api/v1/
path('api/v1/health/', views.api_healthcheck, name='api_healthcheck'),
path('api/v1/forward/<str:action_name>/', views.api_forward_router, name='api_forward_router'),
path('api/v1/reverse/<str:action_name>/', views.api_reverse_router, name='api_reverse_router'),
path('api/v1/logs/', views.api_logs_export, name='api_logs_export'),
]

650
shipping/views.py Normal file
View file

@ -0,0 +1,650 @@
import json
import time
from django.shortcuts import render, redirect, get_object_or_404
from django.http import JsonResponse, HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.db.models import Q
from .models import ActivityLog
from .sfx_client import execute_sfx_call, get_base_url
def index_redirect(request):
return redirect('forward_view')
def forward_view(request):
if request.method == 'POST':
action = request.POST.get('action_type')
env = request.POST.get('environment', 'staging')
token = request.POST.get('api_token', 'sample_staging_token_12345')
base_url = get_base_url(env)
headers = {
'Authorization': f'Token {token.strip()}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
# Handle Mock/Simulation Mode toggle if real upstream fails or is blocked
is_mock = request.POST.get('use_mock', 'false') == 'true'
if action == 'serviceability':
pincodes = request.POST.get('pincodes', '560077,110009')
service = request.POST.get('service', 'express')
url = f"{base_url}/v1/clients/serviceability/?service={service}&pincodes={pincodes}"
if is_mock:
res_data = {
"status": "success",
"data": {
"pincodes": [
{"pincode": "560077", "city": "Bengaluru", "serviceable": True, "cod_available": True},
{"pincode": "110009", "city": "New Delhi", "serviceable": True, "cod_available": True}
]
}
}
log_res = record_mock_call('FORWARD', 'Serviceability Check', 'GET', url, headers, None, 200, res_data)
return JsonResponse(log_res)
res = execute_sfx_call('FORWARD', 'Serviceability Check', 'GET', url, headers, env=env)
return JsonResponse(res)
elif action == 'generate_awb':
count = int(request.POST.get('count', 1))
client_code = request.POST.get('client_code', 'DEMO_CLIENT')
url = f"{base_url}/v3/clients/generate_marketplace_awb/"
payload = {"client_code": client_code, "count": count}
if is_mock:
res_data = {"status": "success", "data": {"awb_numbers": [f"SFX{int(time.time())}99"]}}
log_res = record_mock_call('FORWARD', 'Generate AWB', 'POST', url, headers, payload, 200, res_data)
return JsonResponse(log_res)
res = execute_sfx_call('FORWARD', 'Generate AWB', 'POST', url, headers, payload, env=env)
return JsonResponse(res)
elif action == 'create_marketplace_order':
raw_payload = request.POST.get('payload', '{}')
try:
payload = json.loads(raw_payload)
except Exception as e:
return JsonResponse({'status': 'ERROR', 'response': f'Invalid JSON payload: {e}'}, status=400)
url = f"{base_url}/v3/clients/orders/"
if is_mock:
awb = payload.get('order_details', {}).get('awb_number') or f"SFX{int(time.time())}01"
order_id = payload.get('order_details', {}).get('client_order_id') or "ORD-MKT-101"
res_data = {"status": "success", "message": "Order created successfully", "data": {"client_order_id": order_id, "awb_number": awb, "order_status": "new"}}
log_res = record_mock_call('FORWARD', 'Create Marketplace Order', 'POST', url, headers, payload, 201, res_data)
return JsonResponse(log_res)
res = execute_sfx_call('FORWARD', 'Create Marketplace Order', 'POST', url, headers, payload, env=env)
return JsonResponse(res)
elif action == 'create_warehouse_order':
raw_payload = request.POST.get('payload', '{}')
try:
payload = json.loads(raw_payload)
except Exception as e:
return JsonResponse({'status': 'ERROR', 'response': f'Invalid JSON payload: {e}'}, status=400)
url = f"{base_url}/v3/clients/orders/"
if is_mock:
awb = payload.get('order_details', {}).get('awb_number') or f"SFX{int(time.time())}02"
order_id = payload.get('order_details', {}).get('client_order_id') or "ORD-WH-505"
res_data = {"status": "success", "message": "Warehouse order created", "data": {"client_order_id": order_id, "awb_number": awb, "order_status": "new"}}
log_res = record_mock_call('FORWARD', 'Create Warehouse Order', 'POST', url, headers, payload, 201, res_data)
return JsonResponse(log_res)
res = execute_sfx_call('FORWARD', 'Create Warehouse Order', 'POST', url, headers, payload, env=env)
return JsonResponse(res)
elif action == 'track_single':
awb = request.POST.get('awb_number', 'SFX9928374102')
url = f"{base_url}/v4/clients/orders/{awb}/track/"
if is_mock:
res_data = {
"status": "success",
"data": {
"awb_number": awb,
"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"}
]
}
}
log_res = record_mock_call('FORWARD', 'Track Single Order V4', 'GET', url, headers, None, 200, res_data)
return JsonResponse(log_res)
res = execute_sfx_call('FORWARD', 'Track Single Order V4', 'GET', url, headers, env=env)
return JsonResponse(res)
elif action == 'track_bulk':
awbs = request.POST.get('awb_numbers', 'SFX9928374102,SFX8817263541').split(',')
url = f"{base_url}/v4/clients/bulk_track/"
payload = {"awb_numbers": [a.strip() for a in awbs if a.strip()]}
if is_mock:
res_data = {
"status": "success",
"data": [
{"awb_number": a, "current_status": "in_transit"} for a in payload["awb_numbers"]
]
}
log_res = record_mock_call('FORWARD', 'Track Bulk Orders V4', 'POST', url, headers, payload, 200, res_data)
return JsonResponse(log_res)
res = execute_sfx_call('FORWARD', 'Track Bulk Orders V4', 'POST', url, headers, payload, env=env)
return JsonResponse(res)
elif action == 'cancel_order':
client_order_id = request.POST.get('client_order_id', 'ORD-MKT-101')
awb = request.POST.get('awb_number', 'SFX9928374102')
reason = request.POST.get('reason', 'Customer requested cancellation')
url = f"{base_url}/v3/clients/orders/cancel/"
payload = {"client_order_id": client_order_id, "awb_number": awb, "reason": reason}
if is_mock:
res_data = {"status": "success", "message": "Order cancelled successfully", "data": {"awb_number": awb, "status": "cancelled"}}
log_res = record_mock_call('FORWARD', 'Cancel Order', 'POST', url, headers, payload, 200, res_data)
return JsonResponse(log_res)
res = execute_sfx_call('FORWARD', 'Cancel Order', 'POST', url, headers, payload, env=env)
return JsonResponse(res)
elif action == 'escalation':
awb = request.POST.get('awb_number', 'SFX9928374102')
category = request.POST.get('category', 'EXPEDITE_PICKUP')
description = request.POST.get('description', 'Pickup store closing early')
url = f"{base_url}/v1/clients/support/issue/"
payload = {"awb_number": awb, "category": category, "description": description}
if is_mock:
res_data = {"status": "success", "message": "Ticket raised", "data": {"ticket_id": "TKT-99120", "category": category}}
log_res = record_mock_call('FORWARD', 'Raise Escalation Ticket', 'POST', url, headers, payload, 200, res_data)
return JsonResponse(log_res)
res = execute_sfx_call('FORWARD', 'Raise Escalation Ticket', 'POST', url, headers, payload, env=env)
return JsonResponse(res)
elif action == 'get_pod':
awb = request.POST.get('awb_number', 'SFX9928374102')
url = f"{base_url}/v1/clients/pod_details/"
payload = {"awb_number": awb}
if is_mock:
res_data = {"status": "success", "data": {"awb_number": awb, "signature_url": f"https://cdn.shadowfax.in/pod/{awb}.png", "otp_verified": True}}
log_res = record_mock_call('FORWARD', 'Get POD Details', 'POST', url, headers, payload, 200, res_data)
return JsonResponse(log_res)
res = execute_sfx_call('FORWARD', 'Get POD Details', 'POST', url, headers, payload, env=env)
return JsonResponse(res)
elif action == 'generate_qr':
awb = request.POST.get('awb_number', 'SFX9928374102')
cod_amount = float(request.POST.get('cod_amount', 1499.0))
url = f"{base_url}/v2/clients/generate_qr_code/"
payload = {"awb_number": awb, "cod_amount": cod_amount}
if is_mock:
res_data = {"status": "success", "data": {"awb_number": awb, "cod_amount": cod_amount, "qr_code_string": f"upi://pay?pa=shadowfax@icici&am={cod_amount}"}}
log_res = record_mock_call('FORWARD', 'Generate Dynamic QR Code V2', 'POST', url, headers, payload, 200, res_data)
return JsonResponse(log_res)
res = execute_sfx_call('FORWARD', 'Generate Dynamic QR Code V2', 'POST', url, headers, payload, env=env)
return JsonResponse(res)
# GET Request: render template
recent_logs = ActivityLog.objects.filter(service_type='FORWARD')[:10]
return render(request, 'shipping/forward.html', {'recent_logs': recent_logs})
def reverse_view(request):
if request.method == 'POST':
action = request.POST.get('action_type')
env = request.POST.get('environment', 'staging')
token = request.POST.get('api_token', 'sample_staging_token_12345')
base_url = get_base_url(env)
headers = {
'Authorization': f'Token {token.strip()}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
is_mock = request.POST.get('use_mock', 'false') == 'true'
if action == 'reverse_serviceability':
pincodes = request.POST.get('pincodes', '560077,110009')
url = f"{base_url}/v1/clients/serviceability/?service=reverse&pincodes={pincodes}"
if is_mock:
res_data = {"status": "success", "data": {"pincodes": [{"pincode": "560077", "reverse_pickup_available": True}]}}
log_res = record_mock_call('REVERSE', 'Reverse Serviceability Check', 'GET', url, headers, None, 200, res_data)
return JsonResponse(log_res)
res = execute_sfx_call('REVERSE', 'Reverse Serviceability Check', 'GET', url, headers, env=env)
return JsonResponse(res)
elif action == 'generate_reverse_awb':
count = int(request.POST.get('count', 1))
client_code = request.POST.get('client_code', 'DEMO_CLIENT')
url = f"{base_url}/v3/clients/orders/generate_awb/"
payload = {"client_code": client_code, "count": count}
if is_mock:
res_data = {"status": "success", "data": {"awb_numbers": [f"SFX-REV-{int(time.time())}99"]}}
log_res = record_mock_call('REVERSE', 'Generate Reverse AWB', 'POST', url, headers, payload, 200, res_data)
return JsonResponse(log_res)
res = execute_sfx_call('REVERSE', 'Generate Reverse AWB', 'POST', url, headers, payload, env=env)
return JsonResponse(res)
elif action == 'create_reverse_request':
raw_payload = request.POST.get('payload', '{}')
try:
payload = json.loads(raw_payload)
except Exception as e:
return JsonResponse({'status': 'ERROR', 'response': f'Invalid JSON payload: {e}'}, status=400)
url = f"{base_url}/v3/clients/requests"
if is_mock:
req_id = payload.get('client_request_id') or "REV-ORD-8801"
awb = payload.get('awb_number') or f"SFX-REV-{int(time.time())}01"
res_data = {"status": "success", "message": "Reverse pickup request created", "data": {"client_request_id": req_id, "awb_number": awb, "status": "new"}}
log_res = record_mock_call('REVERSE', 'Create Reverse Pickup Request', 'POST', url, headers, payload, 201, res_data)
return JsonResponse(log_res)
res = execute_sfx_call('REVERSE', 'Create Reverse Pickup Request', 'POST', url, headers, payload, env=env)
return JsonResponse(res)
elif action == 'track_reverse_single':
req_id = request.POST.get('client_request_id', 'REV-ORD-8801')
url = f"{base_url}/v4/clients/requests/{req_id}"
if is_mock:
res_data = {
"status": "success",
"data": {
"client_request_id": req_id,
"current_status": "picked_up",
"qc_status": "PASSED",
"rider_details": {"name": "Ramesh Kumar", "phone": "+919876543210"},
"tracking_history": [
{"status": "new", "timestamp": "2026-09-25T10:00:00Z"},
{"status": "picked_up", "timestamp": "2026-09-25T12:00:00Z", "remarks": "Item passed QC"}
]
}
}
log_res = record_mock_call('REVERSE', 'Track Reverse Pickup V4', 'GET', url, headers, None, 200, res_data)
return JsonResponse(log_res)
res = execute_sfx_call('REVERSE', 'Track Reverse Pickup V4', 'GET', url, headers, env=env)
return JsonResponse(res)
elif action == 'track_reverse_bulk':
req_ids = request.POST.get('client_request_ids', 'REV-ORD-8801,REV-ORD-8802').split(',')
url = f"{base_url}/v4/clients/requests/bulk_query"
payload = {"client_request_ids": [r.strip() for r in req_ids if r.strip()]}
if is_mock:
res_data = {"status": "success", "data": [{"client_request_id": r, "current_status": "picked_up"} for r in payload["client_request_ids"]]}
log_res = record_mock_call('REVERSE', 'Bulk Track Reverse Pickups V4', 'POST', url, headers, payload, 200, res_data)
return JsonResponse(log_res)
res = execute_sfx_call('REVERSE', 'Bulk Track Reverse Pickups V4', 'POST', url, headers, payload, env=env)
return JsonResponse(res)
elif action == 'update_qc':
req_id = request.POST.get('client_request_id', 'REV-ORD-8801')
qc_check = request.POST.get('qc_check', 'true') == 'true'
serial_number = request.POST.get('serial_number', 'SN-9988112233')
url = f"{base_url}/v2/clients/requests/update_qc/"
payload = {"client_request_id": req_id, "qc_check": qc_check, "serial_number": serial_number}
if is_mock:
res_data = {"status": "success", "message": "QC Flag updated", "data": payload}
log_res = record_mock_call('REVERSE', 'Update QC Flag', 'PUT', url, headers, payload, 200, res_data)
return JsonResponse(log_res)
res = execute_sfx_call('REVERSE', 'Update QC Flag', 'PUT', url, headers, payload, env=env)
return JsonResponse(res)
elif action == 'cancel_reverse':
req_id = request.POST.get('client_request_id', 'REV-ORD-8801')
reason = request.POST.get('reason', 'Customer withdrew return request')
url = f"{base_url}/v2/clients/requests/mark_cancel"
payload = {"client_request_id": req_id, "reason": reason}
if is_mock:
res_data = {"status": "success", "message": "Reverse request cancelled", "data": {"client_request_id": req_id, "status": "cancelled"}}
log_res = record_mock_call('REVERSE', 'Cancel Reverse Request', 'POST', url, headers, payload, 200, res_data)
return JsonResponse(log_res)
res = execute_sfx_call('REVERSE', 'Cancel Reverse Request', 'POST', url, headers, payload, env=env)
return JsonResponse(res)
elif action == 'get_reverse_pod':
req_id = request.POST.get('client_request_id', 'REV-ORD-8801')
url = f"{base_url}/v1/clients/pod_details/?client_request_id={req_id}"
if is_mock:
res_data = {"status": "success", "data": {"client_request_id": req_id, "signature_url": f"https://cdn.shadowfax.in/pop/{req_id}.png", "otp_verified": True}}
log_res = record_mock_call('REVERSE', 'Get Proof of Pickup (POP)', 'GET', url, headers, None, 200, res_data)
return JsonResponse(log_res)
res = execute_sfx_call('REVERSE', 'Get Proof of Pickup (POP)', 'GET', url, headers, env=env)
return JsonResponse(res)
recent_logs = ActivityLog.objects.filter(service_type='REVERSE')[:10]
return render(request, 'shipping/reverse.html', {'recent_logs': recent_logs})
def logs_view(request):
query = request.GET.get('q', '')
service_filter = request.GET.get('service', '')
status_filter = request.GET.get('status', '')
logs = ActivityLog.objects.all()
if service_filter:
logs = logs.filter(service_type=service_filter)
if status_filter:
logs = logs.filter(status=status_filter)
if query:
logs = logs.filter(
Q(action_name__icontains=query) |
Q(endpoint_url__icontains=query) |
Q(request_body__icontains=query) |
Q(response_body__icontains=query)
)
return render(request, 'shipping/logs.html', {
'logs': logs[:100],
'query': query,
'service_filter': service_filter,
'status_filter': status_filter
})
def log_detail_api(request, log_id):
log_item = get_object_or_404(ActivityLog, id=log_id)
return JsonResponse({
'id': log_item.id,
'service_type': log_item.service_type,
'action_name': log_item.action_name,
'endpoint_url': log_item.endpoint_url,
'http_method': log_item.http_method,
'request_headers': log_item.request_headers,
'request_body': log_item.request_body,
'response_status_code': log_item.response_status_code,
'response_body': log_item.response_body,
'execution_time_ms': log_item.execution_time_ms,
'status': log_item.status,
'timestamp': log_item.timestamp.isoformat()
})
def clear_logs_view(request):
if request.method == 'POST':
ActivityLog.objects.all().delete()
return redirect('logs_view')
@csrf_exempt
def webhook_listener_api(request):
if request.method == 'POST':
try:
body_unicode = request.body.decode('utf-8')
try:
body_data = json.loads(body_unicode)
body_formatted = json.dumps(body_data, indent=2)
except Exception:
body_formatted = body_unicode
except Exception as e:
body_formatted = f"Raw body error: {e}"
headers_dict = {k: v for k, v in request.META.items() if k.startswith('HTTP_') or k in ['CONTENT_TYPE', 'CONTENT_LENGTH']}
headers_formatted = json.dumps(headers_dict, indent=2)
log_entry = ActivityLog.objects.create(
service_type='WEBHOOK',
action_name='Incoming Webhook Callback',
endpoint_url=request.build_absolute_uri(),
http_method='POST',
request_headers=headers_formatted,
request_body=body_formatted,
response_status_code=200,
response_body=json.dumps({"status": "received", "timestamp": time.time()}),
execution_time_ms=1.5,
status='SUCCESS'
)
return JsonResponse({"status": "success", "message": "Webhook logged", "log_id": log_entry.id})
return JsonResponse({"error": "POST method required"}, status=405)
def webhooks_ui_view(request):
if request.method == 'POST':
# Simulate webhook trigger
event_type = request.POST.get('event_type', 'out_for_delivery')
order_id = request.POST.get('order_id', 'ORD-MKT-2026-101')
awb = request.POST.get('awb_number', 'SFX9928374102')
service_type = request.POST.get('service_type', 'FORWARD')
sim_payload = {
"client_order_id": order_id,
"awb_number": awb,
"status": event_type,
"sub_status": event_type,
"remarks": f"Simulated {service_type} webhook event: {event_type}",
"reason_code": "SIM_OK",
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ")
}
ActivityLog.objects.create(
service_type='WEBHOOK',
action_name=f'Simulated Callback ({service_type}: {event_type})',
endpoint_url='/api/webhook/simulator',
http_method='POST',
request_headers=json.dumps({"Content-Type": "application/json", "X-Simulated": "True"}, indent=2),
request_body=json.dumps(sim_payload, indent=2),
response_status_code=200,
response_body=json.dumps({"status": "simulated_success"}),
execution_time_ms=0.8,
status='SUCCESS'
)
return redirect('webhooks_ui_view')
webhook_logs = ActivityLog.objects.filter(service_type='WEBHOOK')[:30]
return render(request, 'shipping/webhooks.html', {'webhook_logs': webhook_logs})
def record_mock_call(service_type, action_name, method, url, headers, payload, status_code, response_data):
req_body_str = json.dumps(payload, indent=2) if payload else ""
req_headers_str = json.dumps(headers, indent=2) if headers else ""
res_body_str = json.dumps(response_data, indent=2)
log_entry = ActivityLog.objects.create(
service_type=service_type,
action_name=f"{action_name} (Simulation)",
endpoint_url=url,
http_method=method,
request_headers=req_headers_str,
request_body=req_body_str,
response_status_code=status_code,
response_body=res_body_str,
execution_time_ms=3.2,
status='SUCCESS'
)
return {
"log_id": log_entry.id,
"status_code": status_code,
"execution_time_ms": 3.2,
"status": "SUCCESS",
"response": res_body_str
}
# ==============================================================================
# REST API Endpoints for External Microservices (e.g. seller_central_backend)
# ==============================================================================
@csrf_exempt
def api_healthcheck(request):
return JsonResponse({
"status": "healthy",
"service": "Shadowfax Shipping Microservice",
"timestamp": time.time()
})
@csrf_exempt
def api_forward_router(request, action_name):
if request.method not in ['POST', 'GET']:
return JsonResponse({'error': 'POST or GET method required'}, status=405)
try:
data = json.loads(request.body.decode('utf-8')) if request.body else {}
except Exception:
data = {}
token = data.get('token') or request.headers.get('Authorization', '').replace('Token ', '') or 'sample_staging_token_12345'
env = data.get('env') or 'staging'
is_mock = data.get('use_mock', True)
base_url = get_base_url(env)
headers = {
'Authorization': f'Token {token.strip()}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
if action_name == 'serviceability':
pincodes = data.get('pincodes', '560077,110009')
service = data.get('service', 'express')
url = f"{base_url}/v1/clients/serviceability/?service={service}&pincodes={pincodes}"
if is_mock:
res_data = {"status": "success", "data": {"pincodes": [{"pincode": "560077", "city": "Bengaluru", "serviceable": True}]}}
log_res = record_mock_call('FORWARD', 'API: Serviceability Check', 'GET', url, headers, None, 200, res_data)
return JsonResponse(log_res)
return JsonResponse(execute_sfx_call('FORWARD', 'API: Serviceability Check', 'GET', url, headers, env=env))
elif action_name in ['create-order', 'create_order']:
payload = data.get('payload', data)
url = f"{base_url}/v3/clients/orders/"
if is_mock:
awb = payload.get('order_details', {}).get('awb_number') or f"SFX{int(time.time())}01"
order_id = payload.get('order_details', {}).get('client_order_id') or "ORD-MKT-101"
res_data = {"status": "success", "message": "Order created", "data": {"client_order_id": order_id, "awb_number": awb, "order_status": "new"}}
log_res = record_mock_call('FORWARD', 'API: Create Order', 'POST', url, headers, payload, 201, res_data)
return JsonResponse(log_res)
return JsonResponse(execute_sfx_call('FORWARD', 'API: Create Order', 'POST', url, headers, payload, env=env))
elif action_name == 'track':
awb = request.GET.get('awb') or data.get('awb_number', 'SFX9928374102')
url = f"{base_url}/v4/clients/orders/{awb}/track/"
if is_mock:
res_data = {"status": "success", "data": {"awb_number": awb, "current_status": "out_for_delivery"}}
log_res = record_mock_call('FORWARD', 'API: Track Order', 'GET', url, headers, None, 200, res_data)
return JsonResponse(log_res)
return JsonResponse(execute_sfx_call('FORWARD', 'API: Track Order', 'GET', url, headers, env=env))
elif action_name == 'cancel':
payload = data.get('payload', data)
url = f"{base_url}/v3/clients/orders/cancel/"
if is_mock:
res_data = {"status": "success", "message": "Order cancelled"}
log_res = record_mock_call('FORWARD', 'API: Cancel Order', 'POST', url, headers, payload, 200, res_data)
return JsonResponse(log_res)
return JsonResponse(execute_sfx_call('FORWARD', 'API: Cancel Order', 'POST', url, headers, payload, env=env))
return JsonResponse({'error': f'Action {action_name} not supported'}, status=400)
@csrf_exempt
def api_reverse_router(request, action_name):
if request.method not in ['POST', 'GET', 'PUT']:
return JsonResponse({'error': 'POST, GET or PUT method required'}, status=405)
try:
data = json.loads(request.body.decode('utf-8')) if request.body else {}
except Exception:
data = {}
token = data.get('token') or request.headers.get('Authorization', '').replace('Token ', '') or 'sample_staging_token_12345'
env = data.get('env') or 'staging'
is_mock = data.get('use_mock', True)
base_url = get_base_url(env)
headers = {
'Authorization': f'Token {token.strip()}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
if action_name == 'serviceability':
pincodes = data.get('pincodes', '560077,110009')
url = f"{base_url}/v1/clients/serviceability/?service=reverse&pincodes={pincodes}"
if is_mock:
res_data = {"status": "success", "data": {"pincodes": [{"pincode": "560077", "reverse_pickup_available": True}]}}
log_res = record_mock_call('REVERSE', 'API: Reverse Serviceability Check', 'GET', url, headers, None, 200, res_data)
return JsonResponse(log_res)
return JsonResponse(execute_sfx_call('REVERSE', 'API: Reverse Serviceability Check', 'GET', url, headers, env=env))
elif action_name in ['create-request', 'create_request']:
payload = data.get('payload', data)
url = f"{base_url}/v3/clients/requests"
if is_mock:
req_id = payload.get('client_request_id') or "REV-ORD-8801"
awb = payload.get('awb_number') or f"SFX-REV-{int(time.time())}01"
res_data = {"status": "success", "message": "Reverse pickup request created", "data": {"client_request_id": req_id, "awb_number": awb, "status": "new"}}
log_res = record_mock_call('REVERSE', 'API: Create Reverse Request', 'POST', url, headers, payload, 201, res_data)
return JsonResponse(log_res)
return JsonResponse(execute_sfx_call('REVERSE', 'API: Create Reverse Request', 'POST', url, headers, payload, env=env))
elif action_name == 'track':
req_id = request.GET.get('client_request_id') or data.get('client_request_id', 'REV-ORD-8801')
url = f"{base_url}/v4/clients/requests/{req_id}"
if is_mock:
res_data = {"status": "success", "data": {"client_request_id": req_id, "current_status": "picked_up", "qc_status": "PASSED"}}
log_res = record_mock_call('REVERSE', 'API: Track Reverse Request', 'GET', url, headers, None, 200, res_data)
return JsonResponse(log_res)
return JsonResponse(execute_sfx_call('REVERSE', 'API: Track Reverse Request', 'GET', url, headers, env=env))
elif action_name in ['update-qc', 'update_qc']:
payload = data.get('payload', data)
url = f"{base_url}/v2/clients/requests/update_qc/"
if is_mock:
res_data = {"status": "success", "message": "QC Flag updated", "data": payload}
log_res = record_mock_call('REVERSE', 'API: Update QC Flag', 'PUT', url, headers, payload, 200, res_data)
return JsonResponse(log_res)
return JsonResponse(execute_sfx_call('REVERSE', 'API: Update QC Flag', 'PUT', url, headers, payload, env=env))
return JsonResponse({'error': f'Action {action_name} not supported'}, status=400)
@csrf_exempt
def api_logs_export(request):
query = request.GET.get('q', '')
service_filter = request.GET.get('service', '')
logs = ActivityLog.objects.all()
if service_filter:
logs = logs.filter(service_type=service_filter)
if query:
logs = logs.filter(Q(action_name__icontains=query) | Q(request_body__icontains=query) | Q(response_body__icontains=query))
logs_data = [
{
"id": l.id,
"service_type": l.service_type,
"action_name": l.action_name,
"endpoint_url": l.endpoint_url,
"http_method": l.http_method,
"response_status_code": l.response_status_code,
"execution_time_ms": l.execution_time_ms,
"status": l.status,
"timestamp": l.timestamp.isoformat()
} for l in logs[:100]
]
return JsonResponse({"count": len(logs_data), "logs": logs_data})

View file

@ -0,0 +1,21 @@
[Unit]
Description=Shipping Service Beta Service
After=network.target
[Service]
User=ubuntu
WorkingDirectory=/home/ubuntu/shippingservice
EnvironmentFile=/home/ubuntu/shippingservice/.env.beta
ExecStart=/home/ubuntu/shippingservice/venv/bin/gunicorn shippingservice_project.wsgi:application \
--bind 0.0.0.0:8086 \
--workers 3 \
--access-logfile /home/ubuntu/shippingservice/logs/gunicorn-beta-access.log \
--error-logfile /home/ubuntu/shippingservice/logs/gunicorn-beta-error.log \
--log-level info \
--capture-output
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

View file

View file

@ -0,0 +1,16 @@
"""
ASGI config for shippingservice_project project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shippingservice_project.settings')
application = get_asgi_application()

View file

@ -0,0 +1,153 @@
"""
Django settings for shippingservice_project project.
Generated by 'django-admin startproject' using Django 6.1.1.
For more information on this file, see
https://docs.djangoproject.com/en/6.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.1/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-sjt(lare3xpvbqfv$by71n6n5nx-94))6o(ew8*t9y$$&2aspa'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'shipping',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'shippingservice_project.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'shippingservice_project.wsgi.application'
import os
# Database
# https://docs.djangoproject.com/en/6.1/ref/settings/#databases
ENV = os.getenv('DJANGO_ENV', 'beta').lower()
DB_NAME = 'shippingservice_beta' if ENV == 'beta' else 'shippingservice_prod'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('DB_NAME', DB_NAME),
'USER': os.getenv('DB_USER', 'vignesh'),
'PASSWORD': os.getenv('DB_PASSWORD', 'vignesh_secret_pass_2026'),
'HOST': os.getenv('DB_HOST', '16.113.106.152'),
'PORT': os.getenv('DB_PORT', '5432'),
},
'beta': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'shippingservice_beta',
'USER': 'vignesh',
'PASSWORD': 'vignesh_secret_pass_2026',
'HOST': '16.113.106.152',
'PORT': '5432',
},
'prod': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'shippingservice_prod',
'USER': 'vignesh',
'PASSWORD': 'vignesh_secret_pass_2026',
'HOST': '16.113.106.152',
'PORT': '5432',
}
}
# Password validation
# https://docs.djangoproject.com/en/6.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/6.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.1/howto/static-files/
STATIC_URL = 'static/'
# Email
# https://docs.djangoproject.com/en/6.1/topics/email/#topic-email-configuration
MAILERS = {
'default': {
'BACKEND': 'django.core.mail.backends.console.EmailBackend',
},
}

View file

@ -0,0 +1,7 @@
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('shipping.urls')),
]

View file

@ -0,0 +1,16 @@
"""
WSGI config for shippingservice_project project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'shippingservice_project.settings')
application = get_wsgi_application()