feat: initialize Django shipping service project with Shadowfax API integration and webhook simulator
Some checks failed
Deploy Beta (NATIVE) / deploy (push) Failing after 39s
Some checks failed
Deploy Beta (NATIVE) / deploy (push) Failing after 39s
This commit is contained in:
commit
5c85198e8b
43 changed files with 8406 additions and 0 deletions
144
.forgejo/workflows/beta.yml
Normal file
144
.forgejo/workflows/beta.yml
Normal 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
BIN
db.sqlite3
Normal file
Binary file not shown.
563
documentation/forward_integration.md
Normal file
563
documentation/forward_integration.md
Normal file
|
|
@ -0,0 +1,563 @@
|
|||
# Shadowfax Unified API (SFX) — Complete Forward Integration Document
|
||||
|
||||
> **Document Scope**: End-to-End Technical Integration Guide for Shadowfax Logistics Platform
|
||||
> **Supported Models**: Marketplace Seller Pickup & Warehouse Hub Dispatch
|
||||
> **API Version**: Unified API v1 / v3 / v4
|
||||
> **Document Status**: Production Ready
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary & Architecture
|
||||
|
||||
Shadowfax (SFX) Unified API offers a single, standardized REST interface designed to streamline forward logistics operations across e-commerce marketplaces, brand storefronts, quick-commerce networks, and regional fulfillment hubs. The platform automates every stage of the shipment lifecycle—from pincode serviceability and pre-allocated AWB numbers to multi-location order placement, real-time shipment tracking, active escalation handling, digital proof of delivery (POD), dynamic COD QR codes, and real-time webhook callback notifications.
|
||||
|
||||
### Operational Models Breakdown
|
||||
1. **Marketplace Model (Seller Pickup & Delivery)**:
|
||||
- **First Mile**: Shadowfax delivery partners pick up shipments directly from distributed seller stores, retail outlets, or micro-warehouses.
|
||||
- **Middle Mile**: Shipments are routed via local Origin Hubs and linehaul Distribution Centers (DCs).
|
||||
- **Last Mile**: Delivered to the end customer address.
|
||||
- **Reverse / RTS**: If delivery fails after max attempts or customer rejection, the order triggers Return to Seller (RTS) and is returned to the origin pickup location.
|
||||
2. **Warehouse Model (Hub Dispatch & Delivery)**:
|
||||
- **First Mile / Linehaul**: Shipments originate directly from centralized client warehouses or regional distribution hubs and are handed over to Shadowfax.
|
||||
- **Last Mile**: Processed through Destination Hubs and delivered to the end customer.
|
||||
- **Reverse / RTO**: If undeliverable, shipments enter Return to Origin (RTO) and return to the client warehouse.
|
||||
|
||||
### Environments & Host URLs
|
||||
| Environment | Base URL | Purpose |
|
||||
| :--- | :--- | :--- |
|
||||
| **Staging / Sandbox** | `https://dale.staging.shadowfax.in` | Developer integration, QA testing & test pincodes (`110009`, `560077`, `560007`) |
|
||||
| **Production** | `https://flash.shadowfax.in` | Live order processing & operational delivery management |
|
||||
|
||||
---
|
||||
|
||||
## 2. Technical Glossary & Core Logistics Concepts
|
||||
|
||||
### Facility Network Architecture
|
||||
- **Distribution Center (DC)**: Large sorting facilities processing bulk intracity and intercity linehaul shipments.
|
||||
- **Hub**: Neighborhood-level delivery centers managing rider assignment for last-mile delivery and first-mile seller pickup.
|
||||
- **Origin Hub**: The initial Shadowfax facility receiving packages directly from sellers or micro-warehouses.
|
||||
- **Origin DC**: The regional distribution center receiving packages from Origin Hubs or client primary warehouses.
|
||||
- **Destination DC**: The sorting hub in the recipient city prior to local hub distribution.
|
||||
- **Destination Hub**: The final delivery hub from which last-mile riders execute customer delivery.
|
||||
- **Via DC**: Intermediate transit sorting hubs where manifest bags pass through without unbagging.
|
||||
|
||||
### Logistics Identifiers & Artifacts
|
||||
- **Air Waybill (AWB)**: Unique 10-14 digit tracking identification number assigned to every parcel.
|
||||
- **Letter of Receipt (LOR)**: Formal digital acknowledgement issued during physical handover of bulk parcels.
|
||||
- **Proof of Delivery (POD)**: Digital evidence (recipient signature, photo timestamp, or OTP) confirming delivery.
|
||||
- **COD / Cash on Delivery**: Collect-on-delivery payments collected via cash or dynamic UPI QR codes.
|
||||
|
||||
---
|
||||
|
||||
## 3. Authentication & Global HTTP Specifications
|
||||
|
||||
Shadowfax API uses **HTTP Header Token Authentication**. Production and Staging API tokens are provisioned by the Shadowfax Integration Team.
|
||||
|
||||
### Mandatory Request Headers
|
||||
```http
|
||||
Authorization: Token <YOUR_PROVISIONED_API_TOKEN>
|
||||
Content-Type: application/json
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
### Standard HTTP Status Codes
|
||||
| HTTP Code | Category | Meaning & Operational Handling |
|
||||
| :--- | :--- | :--- |
|
||||
| `200 OK` | Success | Request succeeded. Response body contains order status or payload data. |
|
||||
| `201 Created` | Success | Order created or AWB generated successfully. |
|
||||
| `400 Bad Request` | Client Error | Schema validation failure or missing mandatory fields. Check field types. |
|
||||
| `401 Unauthorized` | Security Error | Missing or invalid API Token in `Authorization` header. |
|
||||
| `404 Not Found` | Resource Error | AWB number or Order ID does not exist in Shadowfax system. |
|
||||
| `422 Unprocessable` | Business Error | Non-serviceable pincode, duplicate `client_order_id`, or invalid state transition. |
|
||||
| `500 Server Error` | System Error | Downstream system unavailable; execute exponential backoff retry. |
|
||||
|
||||
---
|
||||
|
||||
## 4. Order Lifecycle & State Transitions
|
||||
|
||||
### 4.1 Marketplace Order Lifecycle States
|
||||
| State Name | Status Description | Next Valid States |
|
||||
| :--- | :--- | :--- |
|
||||
| `new` | Order created in Shadowfax system; awaiting seller pickup assignment. | `pickup_assigned`, `cancelled` |
|
||||
| `pickup_assigned` | Rider assigned for seller pickup. | `pickup_rescheduled`, `picked_up`, `pickup_cancelled` |
|
||||
| `pickup_rescheduled` | Seller requested pickup deferral / pickup hold. | `pickup_assigned`, `pickup_cancelled` |
|
||||
| `picked_up` | Package collected from seller; in transit to Origin Hub. | `in_transit` |
|
||||
| `in_transit` | Moving between Origin Hub, DCs, and Destination Hub. | `out_for_delivery` |
|
||||
| `out_for_delivery` | Package with last-mile delivery rider for customer attempt. | `delivered`, `delivery_rescheduled`, `rts_initiated` |
|
||||
| `delivery_rescheduled` | Delivery attempt failed or customer requested date change. | `out_for_delivery`, `rts_initiated` |
|
||||
| `delivered` | Package successfully delivered to customer. | *Terminal State* |
|
||||
| `cancelled` | Order cancelled prior to pickup. | *Terminal State* |
|
||||
| `rts_initiated` | Return to Seller initiated after max delivery attempts or rejection. | `rts_in_transit` |
|
||||
| `rts_in_transit` | Package returning to seller location. | `rts_out_for_delivery` |
|
||||
| `rts_out_for_delivery` | Rider out to hand over returned package back to seller. | `rts_delivered` |
|
||||
| `rts_delivered` | Package successfully handed back to seller. | *Terminal State* |
|
||||
|
||||
### 4.2 Warehouse Order Lifecycle States
|
||||
| State Name | Status Description | Next Valid States |
|
||||
| :--- | :--- | :--- |
|
||||
| `new` | Order created for warehouse fulfillment. | `in_transit`, `cancelled` |
|
||||
| `in_transit` | In transit from client warehouse to destination hub. | `out_for_delivery` |
|
||||
| `out_for_delivery` | Package out for customer delivery attempt. | `delivered`, `delivery_rescheduled`, `rto_initiated` |
|
||||
| `delivery_rescheduled` | Customer requested alternate delivery slot or delivery hold. | `out_for_delivery`, `rto_initiated` |
|
||||
| `delivered` | Successfully delivered to customer. | *Terminal State* |
|
||||
| `cancelled` | Order cancelled before dispatch. | *Terminal State* |
|
||||
| `rto_initiated` | Return to Origin initiated. | `rto_in_transit` |
|
||||
| `rto_in_transit` | Package moving back to client warehouse. | `rto_delivered` |
|
||||
| `rto_delivered` | Package returned and verified at client warehouse. | *Terminal State* |
|
||||
|
||||
---
|
||||
|
||||
## 5. Push Callback API (Webhooks)
|
||||
|
||||
Shadowfax provides real-time event notifications via HTTP POST webhooks pushed directly to the client's configured endpoint whenever a shipment transitions state.
|
||||
|
||||
### Webhook Request Headers
|
||||
```http
|
||||
Content-Type: application/json
|
||||
X-Shadowfax-Signature: <HMAC_SHA256_HASH>
|
||||
```
|
||||
|
||||
### Webhook Standard Payload Schema
|
||||
```json
|
||||
{
|
||||
"client_order_id": "ORD-2026-99012",
|
||||
"awb_number": "SFX10928374651",
|
||||
"status": "out_for_delivery",
|
||||
"sub_status": "out_for_delivery",
|
||||
"remarks": "Package out for delivery with rider Rajesh (Ph: +919876543210)",
|
||||
"reason_code": "",
|
||||
"timestamp": "2026-09-25T14:30:00Z",
|
||||
"location": {
|
||||
"latitude": 12.9716,
|
||||
"longitude": 77.5946,
|
||||
"city": "Bengaluru"
|
||||
},
|
||||
"attempt_count": 1,
|
||||
"pickup_details": {
|
||||
"pickup_id": "PICKUP-8812",
|
||||
"name": "Acme Electronics Hub"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Webhook Event State Mappings
|
||||
| Webhook `status` | Marketplace State Equivalent | Warehouse State Equivalent | Trigger Event |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `pickup_assigned` | `pickup_assigned` | N/A | Pickup executive assigned to seller. |
|
||||
| `picked_up` | `picked_up` | N/A | Package scanned at seller location. |
|
||||
| `in_transit` | `in_transit` | `in_transit` | Package arrived or departed DC/Hub. |
|
||||
| `out_for_delivery` | `out_for_delivery` | `out_for_delivery` | Package scanned onto rider runsheet. |
|
||||
| `delivered` | `delivered` | `delivered` | Successful delivery to customer. |
|
||||
| `delivery_rescheduled`| `delivery_rescheduled`| `delivery_rescheduled`| Delivery attempt deferred with reason code. |
|
||||
| `rts_initiated` | `rts_initiated` | N/A | Return to seller triggered. |
|
||||
| `rto_initiated` | N/A | `rto_initiated` | Return to origin warehouse triggered. |
|
||||
|
||||
---
|
||||
|
||||
## 6. Endpoints Reference & Full API Specifications
|
||||
|
||||
### 6.1 Serviceability API
|
||||
**HTTP Method**: `GET`
|
||||
**Endpoint Path**: `/v1/clients/serviceability/?service={service}&page={page}&count={count}&pincodes={pincodes}`
|
||||
**Description**: Fetches list of serviceable pincodes or verifies if a specific destination pincode is active for delivery/pickup.
|
||||
|
||||
#### Query Parameters
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `service` | String | No | Service filter (e.g. `express`, `surface`, `reverse`). |
|
||||
| `pincodes` | String | No | Comma-separated list of 6-digit Indian pincodes to query. |
|
||||
| `page` | Integer | No | Page number for paginated pincode index (Default: `1`). |
|
||||
| `count` | Integer | No | Number of records per page (Default: `100`, Max: `1000`). |
|
||||
|
||||
#### Sample Response Payload (`200 OK`)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Serviceability retrieved successfully",
|
||||
"data": {
|
||||
"total_count": 2,
|
||||
"page": 1,
|
||||
"pincodes": [
|
||||
{
|
||||
"pincode": "560077",
|
||||
"city": "Bengaluru",
|
||||
"state": "Karnataka",
|
||||
"serviceable": true,
|
||||
"pickup_available": true,
|
||||
"delivery_available": true,
|
||||
"cod_available": true,
|
||||
"prepaid_available": true
|
||||
},
|
||||
{
|
||||
"pincode": "110009",
|
||||
"city": "New Delhi",
|
||||
"state": "Delhi",
|
||||
"serviceable": true,
|
||||
"pickup_available": true,
|
||||
"delivery_available": true,
|
||||
"cod_available": true,
|
||||
"prepaid_available": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 AWB Generation API
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v3/clients/generate_marketplace_awb/`
|
||||
**Description**: Pre-allocates a Shadowfax AWB tracking number prior to creating the complete order payload.
|
||||
|
||||
#### Sample Request Payload
|
||||
```json
|
||||
{
|
||||
"client_code": "ACME_RETAIL",
|
||||
"count": 1
|
||||
}
|
||||
```
|
||||
|
||||
#### Sample Response Payload (`200 OK`)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"awb_numbers": [
|
||||
"SFX9928374102"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 Marketplace Order Creation API (Seller Pickup Model)
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v3/clients/orders/`
|
||||
**Description**: Creates a seller pickup request and customer delivery order in a single API call.
|
||||
|
||||
#### Request Payload Parameter Matrix
|
||||
##### 1. `order_details` Object
|
||||
| Field Name | Type | Required | Validation & Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `client_order_id` | String | **Yes** | Client internal order ID (Must be unique across system). |
|
||||
| `awb_number` | String | No | Pre-generated Shadowfax AWB number. Auto-generated if omitted. |
|
||||
| `actual_weight` | Number | **Yes** | Weight of shipment in grams (e.g. `500.0`). |
|
||||
| `volumetric_weight` | Number | No | Volumetric weight in grams `(L * W * H) / 5000` (Default: `0.0`). |
|
||||
| `product_value` | Number | **Yes** | Total invoice product value in INR excluding tax. |
|
||||
| `payment_mode` | String | **Yes** | Payment type: `"Prepaid"` or `"COD"`. |
|
||||
| `cod_amount` | Number | **Yes** | COD amount to collect from recipient (`0` if `Prepaid`). |
|
||||
| `promised_delivery_date` | String | No | Expected delivery date format `YYYY-MM-DD`. |
|
||||
|
||||
##### 2. `customer_details` Object
|
||||
| Field Name | Type | Required | Validation & Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `name` | String | **Yes** | Recipient full name. |
|
||||
| `phone` | String | **Yes** | 10-digit mobile number of recipient. |
|
||||
| `address_line_1` | String | **Yes** | Building, street, flat number. |
|
||||
| `address_line_2` | String | No | Landmark, locality. |
|
||||
| `pincode` | String | **Yes** | 6-digit serviceable delivery pincode. |
|
||||
| `city` | String | **Yes** | Destination city. |
|
||||
| `state` | String | **Yes** | Destination state. |
|
||||
|
||||
##### 3. `pickup_details` Object
|
||||
| Field Name | Type | Required | Validation & Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `name` | String | **Yes** | Seller / Store / Warehouse name. |
|
||||
| `phone` | String | **Yes** | 10-digit contact mobile number of seller. |
|
||||
| `address_line_1` | String | **Yes** | Seller warehouse address. |
|
||||
| `pincode` | String | **Yes** | 6-digit origin pickup pincode. |
|
||||
| `city` | String | **Yes** | Origin city. |
|
||||
| `state` | String | **Yes** | Origin state. |
|
||||
|
||||
##### 4. `rts_details` Object (Return to Seller)
|
||||
| Field Name | Type | Required | Validation & Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `name` | String | **Yes** | Return contact name. |
|
||||
| `phone` | String | **Yes** | Return contact phone number. |
|
||||
| `address_line_1` | String | **Yes** | Return seller address. |
|
||||
| `pincode` | String | **Yes** | 6-digit return pincode. |
|
||||
|
||||
##### 5. `product_details` Array Items
|
||||
| Field Name | Type | Required | Validation & Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `sku_name` | String | **Yes** | Item title or product name. |
|
||||
| `sku_code` | String | **Yes** | Merchant SKU code. |
|
||||
| `price` | Number | **Yes** | Individual SKU price. |
|
||||
| `quantity` | Integer | **Yes** | Quantity of items. |
|
||||
|
||||
#### Complete Marketplace Request Payload Example
|
||||
```json
|
||||
{
|
||||
"order_type": "marketplace",
|
||||
"order_details": {
|
||||
"client_order_id": "ORD-MKT-2026-101",
|
||||
"awb_number": "SFX9928374102",
|
||||
"actual_weight": 750.0,
|
||||
"volumetric_weight": 800.0,
|
||||
"product_value": 1499.0,
|
||||
"payment_mode": "COD",
|
||||
"cod_amount": 1499.0
|
||||
},
|
||||
"customer_details": {
|
||||
"name": "Ananya Sharma",
|
||||
"phone": "9876543210",
|
||||
"address_line_1": "Flat 402, Sunshine Apartments",
|
||||
"address_line_2": "Indiranagar 10th Main",
|
||||
"pincode": "560038",
|
||||
"city": "Bengaluru",
|
||||
"state": "Karnataka"
|
||||
},
|
||||
"pickup_details": {
|
||||
"name": "StyleHub Retail Store",
|
||||
"phone": "9123456789",
|
||||
"address_line_1": "Plot 12, Industrial Area Phase 2",
|
||||
"pincode": "560077",
|
||||
"city": "Bengaluru",
|
||||
"state": "Karnataka"
|
||||
},
|
||||
"rts_details": {
|
||||
"name": "StyleHub Returns Center",
|
||||
"phone": "9123456789",
|
||||
"address_line_1": "Plot 12, Industrial Area Phase 2",
|
||||
"pincode": "560077",
|
||||
"city": "Bengaluru",
|
||||
"state": "Karnataka"
|
||||
},
|
||||
"product_details": [
|
||||
{
|
||||
"sku_name": "Wireless Noise Cancelling Earbuds",
|
||||
"sku_code": "SKU-EAR-001",
|
||||
"price": 1499.0,
|
||||
"quantity": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Sample Response Payload (`201 Created`)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Order created successfully",
|
||||
"data": {
|
||||
"client_order_id": "ORD-MKT-2026-101",
|
||||
"awb_number": "SFX9928374102",
|
||||
"order_status": "new",
|
||||
"created_at": "2026-09-25T14:35:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 Warehouse Order Creation API (Hub Dispatch Model)
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v3/clients/orders/`
|
||||
**Description**: Creates a delivery order for shipments dispatched directly from a client warehouse or hub.
|
||||
|
||||
#### Complete Warehouse Request Payload Example
|
||||
```json
|
||||
{
|
||||
"order_type": "warehouse",
|
||||
"order_details": {
|
||||
"client_order_id": "ORD-WH-2026-505",
|
||||
"awb_number": "SFX8817263541",
|
||||
"actual_weight": 1200.0,
|
||||
"product_value": 3500.0,
|
||||
"payment_mode": "Prepaid",
|
||||
"cod_amount": 0.0
|
||||
},
|
||||
"customer_details": {
|
||||
"name": "Vikram Mehta",
|
||||
"phone": "9988776655",
|
||||
"address_line_1": "House 14, Civil Lines",
|
||||
"pincode": "110054",
|
||||
"city": "New Delhi",
|
||||
"state": "Delhi"
|
||||
},
|
||||
"rto_details": {
|
||||
"name": "Central Warehouse North",
|
||||
"phone": "9811002233",
|
||||
"address_line_1": "Warehouse Complex Hub 3",
|
||||
"pincode": "110042",
|
||||
"city": "New Delhi",
|
||||
"state": "Delhi"
|
||||
},
|
||||
"product_details": [
|
||||
{
|
||||
"sku_name": "Mechanical Keyboard RGB",
|
||||
"sku_code": "SKU-KB-102",
|
||||
"price": 3500.0,
|
||||
"quantity": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 6.5 Single Order Tracking API (V4)
|
||||
**HTTP Method**: `GET`
|
||||
**Endpoint Path**: `/v4/clients/orders/{awb_number}/track/`
|
||||
**Description**: Retrieves comprehensive tracking timeline, current location, rider details, and status for a shipment.
|
||||
|
||||
#### Sample Response Payload (`200 OK`)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"client_order_id": "ORD-MKT-2026-101",
|
||||
"awb_number": "SFX9928374102",
|
||||
"current_status": "out_for_delivery",
|
||||
"expected_delivery_date": "2026-09-25",
|
||||
"rider_details": {
|
||||
"name": "Rajesh Kumar",
|
||||
"phone": "+919876543210"
|
||||
},
|
||||
"tracking_history": [
|
||||
{
|
||||
"status": "new",
|
||||
"location": "Bengaluru Hub",
|
||||
"timestamp": "2026-09-24T10:00:00Z",
|
||||
"remarks": "Order created"
|
||||
},
|
||||
{
|
||||
"status": "picked_up",
|
||||
"location": "StyleHub Store",
|
||||
"timestamp": "2026-09-24T14:30:00Z",
|
||||
"remarks": "Package picked up from seller"
|
||||
},
|
||||
{
|
||||
"status": "in_transit",
|
||||
"location": "Bengaluru Main DC",
|
||||
"timestamp": "2026-09-24T19:00:00Z",
|
||||
"remarks": "Arrived at sorting DC"
|
||||
},
|
||||
{
|
||||
"status": "out_for_delivery",
|
||||
"location": "Indiranagar Hub",
|
||||
"timestamp": "2026-09-25T08:30:00Z",
|
||||
"remarks": "Assigned to rider for delivery"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.6 Bulk Order Tracking API (V4)
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v4/clients/bulk_track/`
|
||||
**Description**: Fetches current tracking status for up to 50 AWB numbers in a single request.
|
||||
|
||||
#### Sample Request Payload
|
||||
```json
|
||||
{
|
||||
"awb_numbers": [
|
||||
"SFX9928374102",
|
||||
"SFX8817263541"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 6.7 Order Cancellation API
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v3/clients/orders/cancel/`
|
||||
**Description**: Cancels a pending order prior to rider pickup or warehouse dispatch.
|
||||
|
||||
#### Sample Request Payload
|
||||
```json
|
||||
{
|
||||
"client_order_id": "ORD-MKT-2026-101",
|
||||
"awb_number": "SFX9928374102",
|
||||
"reason": "Customer requested cancellation"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.8 Escalation & Support Ticket API
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v1/clients/support/issue/`
|
||||
**Description**: Raises operational support tickets directly to Shadowfax ground control team.
|
||||
|
||||
#### Issue Categories Available
|
||||
- `DELAYED_DELIVERY`: Delivery SLA breached or delayed.
|
||||
- `EXPEDITE_PICKUP`: Urgent seller pickup required.
|
||||
- `DELIVERY_DISPUTE`: Customer disputes status marked delivered.
|
||||
- `ADDRESS_UPDATE`: Correcting destination address/phone.
|
||||
|
||||
#### Sample Request Payload
|
||||
```json
|
||||
{
|
||||
"awb_number": "SFX9928374102",
|
||||
"category": "EXPEDITE_PICKUP",
|
||||
"description": "Seller store closing early today at 4 PM, please pick up by 3 PM."
|
||||
}
|
||||
```
|
||||
|
||||
### 6.9 Proof of Delivery (POD) API
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v1/clients/pod_details/`
|
||||
**Description**: Retrieves digital signature image URL, photo proof, or OTP verification details for delivered shipments.
|
||||
|
||||
#### Sample Response Payload (`200 OK`)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"awb_number": "SFX9928374102",
|
||||
"delivery_timestamp": "2026-09-25T11:45:00Z",
|
||||
"signature_url": "https://cdn.shadowfax.in/pod/signatures/SFX9928374102.png",
|
||||
"photo_proof_url": "https://cdn.shadowfax.in/pod/photos/SFX9928374102.jpg",
|
||||
"receiver_name": "Ananya Sharma",
|
||||
"otp_verified": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.10 Dynamic QR Code Generation API (V2)
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v2/clients/generate_qr_code/`
|
||||
**Description**: Generates dynamic UPI payment QR code strings for Cash-on-Delivery collection at customer doorstep.
|
||||
|
||||
#### Sample Response Payload (`200 OK`)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"awb_number": "SFX9928374102",
|
||||
"cod_amount": 1499.0,
|
||||
"qr_code_string": "upi://pay?pa=shadowfax@icici&pn=Shadowfax&am=1499.00&tr=SFX9928374102&tn=COD%20Payment",
|
||||
"qr_code_image_base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Appendix: Standard Reason & Remark Codes Catalog
|
||||
|
||||
Shadowfax uses standardized numerical and string remark codes during exception events. Client system webhooks should parse these codes for automated business logic.
|
||||
|
||||
### 7.1 Delivery On-Hold Remark Codes
|
||||
| Reason Code | Code Description | Suggested Operational Handling |
|
||||
| :--- | :--- | :--- |
|
||||
| `OH_ADDR_INCOMPLETE` | Customer address incomplete or missing landmark. | Trigger SMS/WhatsApp to customer to update address. |
|
||||
| `OH_CUST_UNAVAILABLE` | Customer not available at address. | Reschedule for next business day automatically. |
|
||||
| `OH_COD_NOT_READY` | Customer requested delivery when cash/UPI ready. | Reschedule delivery date per customer request. |
|
||||
| `OH_PINCODE_UNMAPPED` | Destination pincode out of delivery hub zone. | Trigger escalation ticket to reassign hub. |
|
||||
|
||||
### 7.2 Pickup On-Hold Remark Codes
|
||||
| Reason Code | Code Description | Suggested Operational Handling |
|
||||
| :--- | :--- | :--- |
|
||||
| `PU_STORE_CLOSED` | Seller store/warehouse closed during pickup attempt. | Notify seller contact via alert. |
|
||||
| `PU_ITEM_NOT_READY` | Shipment packaging incomplete at seller end. | Reschedule seller pickup window. |
|
||||
| `PU_ADDR_NOT_FOUND` | Seller pickup address untraceable. | Verify pickup coordinates and phone number. |
|
||||
|
||||
### 7.3 Cancellation Reason Codes
|
||||
| Reason Code | Code Description | Originator |
|
||||
| :--- | :--- | :--- |
|
||||
| `CAN_CLIENT_REQ` | Order cancelled by client system via API. | Client |
|
||||
| `CAN_SELLER_DENIED` | Seller denied stock availability during pickup. | Seller |
|
||||
| `CAN_EXCEEDED_ATTEMPTS`| Max delivery attempts reached (RTS/RTO triggered).| Courier |
|
||||
|
||||
---
|
||||
|
||||
## 8. Summary & Next Steps for Integration
|
||||
|
||||
1. **Credential Setup**: Obtain Staging API Token and test pincode list from Shadowfax Integration Team.
|
||||
2. **Webhook Endpoint Registration**: Configure HTTPS POST Webhook listener and verify HMAC signature parsing.
|
||||
3. **End-to-End Sandbox Testing**: Execute Pincode Check $\rightarrow$ Order Creation $\rightarrow$ Tracking $\rightarrow$ Webhook Receive $\rightarrow$ POD retrieval flow.
|
||||
4. **Go-Live Readiness Review**: Obtain sign-off from Shadowfax Logistics Operations for Production URL switch.
|
||||
418
documentation/reverse_integration.md
Normal file
418
documentation/reverse_integration.md
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
# Shadowfax Reverse Pickup API (SFX) — Complete Reverse Integration Document
|
||||
|
||||
> **Document Scope**: Technical Specification & Integration Guide for Shadowfax Reverse Logistics & Customer Return Platform
|
||||
> **Core Capability**: Doorstep Pickups, Quality Check (QC) Enforcement, Warehouse Returns & Callback Synchronization
|
||||
> **API Version**: Reverse API v1 / v2 / v3 / v4
|
||||
> **Document Status**: Production Ready
|
||||
|
||||
---
|
||||
|
||||
## 1. System Architecture & Reverse Logistics Overview
|
||||
|
||||
The Shadowfax (SFX) Reverse Pickup API enables e-commerce merchants, marketplace operators, and brand retailers to automate customer return pickups and doorstep Quality Checks (QC). Unlike forward delivery where parcels originate from seller/warehouse locations, reverse logistics manages customer doorstep pickups, item inspection, hub consolidation, linehaul transport, and final delivery back to the client's return warehouse.
|
||||
|
||||
### Key Operational Capabilities
|
||||
1. **Doorstep Customer Pickup**: Delivery executive collects items directly from the customer's specified pickup address.
|
||||
2. **Configurable Quality Check (QC)**: Executive verifies physical item condition, brand, serial/IMEI numbers, original packaging, and tags prior to accept/reject.
|
||||
3. **Reverse Linehaul & Consolidation**: Collected items are routed through Shadowfax Origin Hubs and linehaul Distribution Centers (DCs).
|
||||
4. **Warehouse Return Delivery**: Final delivery and digital handover of collected return inventory to client fulfillment centers.
|
||||
5. **Real-time Callback Synchronization**: Push webhooks notify client systems instantly upon pickup status change, QC pass/fail events, and warehouse delivery.
|
||||
|
||||
### Environment Base URLs
|
||||
| Environment | Base URL | Purpose |
|
||||
| :--- | :--- | :--- |
|
||||
| **Staging / Sandbox** | `https://dale.staging.shadowfax.in` | Integration testing & test pincodes (`110009`, `560077`, `560007`) |
|
||||
| **Production** | `https://flash.shadowfax.in` | Live reverse order creation & operational execution |
|
||||
|
||||
---
|
||||
|
||||
## 2. Technical Glossary & Reverse Network Facilities
|
||||
|
||||
### Facility Network Architecture
|
||||
- **Pickup Customer Location**: The customer residential or commercial address where the return item is collected.
|
||||
- **Origin Hub**: First Shadowfax local hub receiving the picked-up return item from the delivery rider.
|
||||
- **Origin DC**: Primary sorting Distribution Center processing reverse linehaul routing.
|
||||
- **Destination DC**: Sorting center located near the client return warehouse.
|
||||
- **Destination Hub / Return Hub**: Final Shadowfax hub executing bulk delivery back to the client warehouse.
|
||||
- **Client Return Warehouse**: Final destination facility where returned goods are handed over to the merchant.
|
||||
|
||||
### Reverse Logistics Identifiers
|
||||
- **Client Request ID / Order ID**: Client's unique internal reference for the return request.
|
||||
- **Reverse Air Waybill (AWB)**: Unique tracking number assigned to the reverse shipment.
|
||||
- **QC Parameter**: Inspection rules (e.g., brand check, IMEI match, unused status) defined per item.
|
||||
- **Proof of Pickup (POP) / POD**: Digital signature or photo timestamp confirming return item collection.
|
||||
|
||||
---
|
||||
|
||||
## 3. Authentication & Global HTTP Specifications
|
||||
|
||||
Shadowfax API uses **HTTP Header Token Authentication**. Production and Staging API tokens are provisioned by the Shadowfax Integration Team.
|
||||
|
||||
### Mandatory HTTP Headers
|
||||
```http
|
||||
Authorization: Token <YOUR_PROVISIONED_API_TOKEN>
|
||||
Content-Type: application/json
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
### Standard HTTP Status Codes
|
||||
| HTTP Code | Category | Operational Meaning & Action |
|
||||
| :--- | :--- | :--- |
|
||||
| `200 OK` | Success | Request succeeded. Body contains request details or query results. |
|
||||
| `201 Created` | Success | Reverse pickup request successfully created. |
|
||||
| `400 Bad Request` | Validation Error | Missing mandatory field or invalid data type format. |
|
||||
| `401 Unauthorized` | Security Error | Invalid or missing API token in `Authorization` header. |
|
||||
| `404 Not Found` | Resource Error | Client Request ID or Reverse AWB number does not exist. |
|
||||
| `422 Unprocessable` | Business Rule | Non-serviceable pickup pincode or duplicate `client_request_id`. |
|
||||
| `500 Server Error` | System Exception | Downstream system unavailable; execute retry with exponential backoff. |
|
||||
|
||||
---
|
||||
|
||||
## 4. Reverse Order Lifecycle & Quality Check (QC) Architecture
|
||||
|
||||
### 4.1 Reverse Pickup Lifecycle States
|
||||
| State Name | Status Description | Next Valid States |
|
||||
| :--- | :--- | :--- |
|
||||
| `new` | Pickup request created in Shadowfax system; awaiting rider assignment. | `pickup_assigned`, `cancelled` |
|
||||
| `pickup_assigned` | Rider assigned to visit customer address. | `picked_up`, `qc_failed`, `pickup_rescheduled`, `cancelled` |
|
||||
| `pickup_rescheduled` | Customer requested alternate pickup date/time window. | `pickup_assigned`, `cancelled` |
|
||||
| `picked_up` | Item passed QC checks and collected from customer; in transit to Origin Hub. | `in_transit` |
|
||||
| `qc_failed` | Item failed doorstep QC checks (e.g. wrong brand, damaged item). Pickup rejected. | *Terminal State* |
|
||||
| `in_transit` | Item moving between Shadowfax Hubs and linehaul DCs toward client warehouse. | `out_for_delivery_to_warehouse` |
|
||||
| `out_for_delivery_to_warehouse` | Item assigned to rider for bulk handover at client return warehouse. | `delivered_to_warehouse`, `delivery_rescheduled` |
|
||||
| `delivered_to_warehouse` | Return item verified and received at client warehouse. | *Terminal State* |
|
||||
| `cancelled` | Reverse pickup request cancelled prior to pickup execution. | *Terminal State* |
|
||||
|
||||
### 4.2 Quality Check (QC) Enforcement Rules
|
||||
Shadowfax riders perform doorstep QC based on criteria supplied in the order payload:
|
||||
1. **Brand & Model Match**: Verifies item brand name and model match order manifest.
|
||||
2. **Serial / IMEI Verification**: Matches 15-digit mobile IMEI or product serial number.
|
||||
3. **Physical Damage Inspection**: Checks for cracks, dents, water damage, or missing components.
|
||||
4. **Tag / Packaging Verification**: Confirms original brand tags/box are intact if required.
|
||||
5. **Doorstep Image Capture**: Rider uploads photo proof of item condition during pickup.
|
||||
|
||||
---
|
||||
|
||||
## 5. Push Callback API (Webhooks)
|
||||
|
||||
Shadowfax notifies client systems in real time whenever a reverse pickup transitions state.
|
||||
|
||||
### Webhook Request Headers
|
||||
```http
|
||||
Content-Type: application/json
|
||||
X-Shadowfax-Signature: <HMAC_SHA256_HASH>
|
||||
```
|
||||
|
||||
### Webhook Standard Payload Schema
|
||||
```json
|
||||
{
|
||||
"client_request_id": "REV-ORD-2026-8801",
|
||||
"awb_number": "SFX-REV-99182736",
|
||||
"status": "picked_up",
|
||||
"sub_status": "qc_passed",
|
||||
"remarks": "Item passed physical QC and picked up by rider Ramesh",
|
||||
"reason_code": "",
|
||||
"qc_result": {
|
||||
"qc_passed": true,
|
||||
"brand_matched": true,
|
||||
"imei_matched": true,
|
||||
"serial_number": "SN-9988112233",
|
||||
"images": [
|
||||
"https://cdn.shadowfax.in/qc/photos/REV-ORD-2026-8801_1.jpg"
|
||||
]
|
||||
},
|
||||
"timestamp": "2026-09-25T15:10:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. API Endpoints Reference & Complete Technical Specifications
|
||||
|
||||
### 6.1 Reverse Pincode Serviceability API
|
||||
**HTTP Method**: `GET`
|
||||
**Endpoint Path**: `/v1/clients/serviceability/?service=reverse&page={page}&count={count}&pincodes={pincodes}`
|
||||
**Description**: Verifies if customer pickup pincodes are active for reverse pickup operations.
|
||||
|
||||
#### Query Parameters
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `service` | String | **Yes** | Pass `"reverse"` for reverse pickup serviceability query. |
|
||||
| `pincodes` | String | No | Comma-separated 6-digit Indian pincodes to check. |
|
||||
| `page` | Integer | No | Page number for paginated results (Default: `1`). |
|
||||
| `count` | Integer | No | Records per page (Default: `100`). |
|
||||
|
||||
#### Sample Response Payload (`200 OK`)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"total_count": 1,
|
||||
"pincodes": [
|
||||
{
|
||||
"pincode": "560077",
|
||||
"city": "Bengaluru",
|
||||
"state": "Karnataka",
|
||||
"reverse_pickup_available": true,
|
||||
"max_weight_grams": 5000.0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Reverse AWB Generation API
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v3/clients/orders/generate_awb/`
|
||||
**Description**: Pre-allocates tracking numbers for reverse pickup shipments.
|
||||
|
||||
#### Sample Request Payload
|
||||
```json
|
||||
{
|
||||
"client_code": "ACME_RETAIL",
|
||||
"count": 1
|
||||
}
|
||||
```
|
||||
|
||||
#### Sample Response Payload (`200 OK`)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"awb_numbers": [
|
||||
"SFX-REV-99182736"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 Reverse Pickup Request Creation API
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v3/clients/requests`
|
||||
**Description**: Creates a customer doorstep pickup request with embedded QC parameters.
|
||||
|
||||
#### Request Payload Parameter Matrix
|
||||
##### 1. Root Level & Order Identification
|
||||
| Field Name | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `client_request_id` | String | **Yes** | Client internal unique return request identifier. |
|
||||
| `awb_number` | String | No | Pre-allocated reverse AWB number. Auto-generated if omitted. |
|
||||
| `actual_weight` | Number | **Yes** | Expected weight of return item in grams. |
|
||||
| `product_value` | Number | **Yes** | Invoice price of item being returned in INR. |
|
||||
| `pickup_date` | String | No | Scheduled pickup date format `YYYY-MM-DD`. |
|
||||
|
||||
##### 2. `pickup_details` (Customer Location)
|
||||
| Field Name | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `name` | String | **Yes** | Customer full name. |
|
||||
| `phone` | String | **Yes** | 10-digit mobile number of customer. |
|
||||
| `address_line_1` | String | **Yes** | Customer street address and flat/house number. |
|
||||
| `pincode` | String | **Yes** | 6-digit serviceable pickup pincode. |
|
||||
| `city` | String | **Yes** | Pickup city. |
|
||||
| `state` | String | **Yes** | Pickup state. |
|
||||
|
||||
##### 3. `return_details` (Client Warehouse Destination)
|
||||
| Field Name | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `name` | String | **Yes** | Warehouse return hub name. |
|
||||
| `phone` | String | **Yes** | Contact phone number of return warehouse. |
|
||||
| `address_line_1` | String | **Yes** | Warehouse street address. |
|
||||
| `pincode` | String | **Yes** | 6-digit warehouse pincode. |
|
||||
| `city` | String | **Yes** | Warehouse city. |
|
||||
| `state` | String | **Yes** | Warehouse state. |
|
||||
|
||||
##### 4. `qc_details` Object (Quality Check Instructions)
|
||||
| Field Name | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `qc_check` | Boolean | **Yes** | `true` to enforce doorstep QC inspection. |
|
||||
| `brand_name` | String | No | Expected brand name on item label. |
|
||||
| `item_name` | String | **Yes** | SKU name / description. |
|
||||
| `serial_number` | String | No | Expected product serial or IMEI number. |
|
||||
| `image_required` | Boolean | No | `true` if rider must take photo proof during QC. |
|
||||
|
||||
#### Complete Reverse Pickup Request Payload Example
|
||||
```json
|
||||
{
|
||||
"client_request_id": "REV-ORD-2026-8801",
|
||||
"awb_number": "SFX-REV-99182736",
|
||||
"actual_weight": 500.0,
|
||||
"product_value": 2999.0,
|
||||
"pickup_details": {
|
||||
"name": "Kavita Reddy",
|
||||
"phone": "9876501234",
|
||||
"address_line_1": "Apartment 102, Green Glen Layout",
|
||||
"address_line_2": "Bellandur",
|
||||
"pincode": "560103",
|
||||
"city": "Bengaluru",
|
||||
"state": "Karnataka"
|
||||
},
|
||||
"return_details": {
|
||||
"name": "Acme Central Returns Warehouse",
|
||||
"phone": "9811004455",
|
||||
"address_line_1": "Logistics Park Warehouse 4B",
|
||||
"pincode": "560077",
|
||||
"city": "Bengaluru",
|
||||
"state": "Karnataka"
|
||||
},
|
||||
"qc_details": {
|
||||
"qc_check": true,
|
||||
"brand_name": "SoundCore",
|
||||
"item_name": "Over-Ear Headphones Black",
|
||||
"serial_number": "SN-9988112233",
|
||||
"image_required": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Sample Response Payload (`201 Created`)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Reverse pickup request created successfully",
|
||||
"data": {
|
||||
"client_request_id": "REV-ORD-2026-8801",
|
||||
"awb_number": "SFX-REV-99182736",
|
||||
"status": "new",
|
||||
"created_at": "2026-09-25T15:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 Reverse Order Cancellation API
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v2/clients/requests/mark_cancel`
|
||||
**Description**: Cancels a pending customer pickup request before rider execution.
|
||||
|
||||
#### Sample Request Payload
|
||||
```json
|
||||
{
|
||||
"client_request_id": "REV-ORD-2026-8801",
|
||||
"reason": "Customer withdrew return request"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.5 Single Pickup Request Tracking API (V4)
|
||||
**HTTP Method**: `GET`
|
||||
**Endpoint Path**: `/v4/clients/requests/{client_request_id}`
|
||||
**Description**: Retrieves tracking timeline, rider contact, QC status, and location for a reverse pickup.
|
||||
|
||||
#### Sample Response Payload (`200 OK`)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"client_request_id": "REV-ORD-2026-8801",
|
||||
"awb_number": "SFX-REV-99182736",
|
||||
"current_status": "picked_up",
|
||||
"qc_status": "PASSED",
|
||||
"rider_details": {
|
||||
"name": "Ramesh Kumar",
|
||||
"phone": "+919876543210"
|
||||
},
|
||||
"tracking_history": [
|
||||
{
|
||||
"status": "new",
|
||||
"timestamp": "2026-09-25T15:00:00Z",
|
||||
"remarks": "Reverse pickup requested"
|
||||
},
|
||||
{
|
||||
"status": "pickup_assigned",
|
||||
"timestamp": "2026-09-25T15:15:00Z",
|
||||
"remarks": "Assigned to rider Ramesh"
|
||||
},
|
||||
{
|
||||
"status": "picked_up",
|
||||
"timestamp": "2026-09-25T16:00:00Z",
|
||||
"remarks": "Item passed QC check and collected from customer"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.6 Bulk Pickup Request Tracking API (V4)
|
||||
**HTTP Method**: `POST`
|
||||
**Endpoint Path**: `/v4/clients/requests/bulk_query`
|
||||
**Description**: Batch queries status for up to 50 reverse pickup requests.
|
||||
|
||||
#### Sample Request Payload
|
||||
```json
|
||||
{
|
||||
"client_request_ids": [
|
||||
"REV-ORD-2026-8801",
|
||||
"REV-ORD-2026-8802"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 6.7 Update QC Flag API
|
||||
**HTTP Method**: `PUT`
|
||||
**Endpoint Path**: `/v2/clients/requests/update_qc/`
|
||||
**Description**: Updates QC verification flags or serial number requirements prior to pickup attempt.
|
||||
|
||||
#### Sample Request Payload
|
||||
```json
|
||||
{
|
||||
"client_request_id": "REV-ORD-2026-8801",
|
||||
"qc_check": true,
|
||||
"serial_number": "SN-9988112244"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.8 Proof of Pickup / POD API
|
||||
**HTTP Method**: `GET`
|
||||
**Endpoint Path**: `/v1/clients/pod_details/?client_request_id={client_request_id}`
|
||||
**Description**: Retrieves digital signature image URL and customer OTP verification status for completed pickups.
|
||||
|
||||
#### Sample Response Payload (`200 OK`)
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"client_request_id": "REV-ORD-2026-8801",
|
||||
"pickup_timestamp": "2026-09-25T16:00:00Z",
|
||||
"signature_url": "https://cdn.shadowfax.in/pop/signatures/REV-ORD-2026-8801.png",
|
||||
"qc_photos": [
|
||||
"https://cdn.shadowfax.in/qc/photos/REV-ORD-2026-8801_1.jpg"
|
||||
],
|
||||
"otp_verified": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Appendix: Standard Reverse Remark & QC Fail Codes Catalog
|
||||
|
||||
Shadowfax returns standardized remark and reason codes during reverse pickup exceptions.
|
||||
|
||||
### 7.1 Quality Check (QC) Failure Reason Codes
|
||||
| Reason Code | Description | Customer & System Handling |
|
||||
| :--- | :--- | :--- |
|
||||
| `QC_FAIL_BRAND_MISMATCH` | Item brand does not match order manifest. | Re-check customer item or reject return request. |
|
||||
| `QC_FAIL_SERIAL_MISMATCH` | Serial/IMEI number on item differs from manifest. | Verify original order invoice serial number. |
|
||||
| `QC_FAIL_DAMAGED_ITEM` | Item exhibits physical damage not recorded in return request. | Notify merchant customer support. |
|
||||
| `QC_FAIL_MISSING_ACC` | Accessories/chargers missing from return package. | Inform customer to provide complete box contents. |
|
||||
| `QC_FAIL_USED_ITEM` | Item shows clear signs of usage (tags removed). | Reject return per merchant policy. |
|
||||
|
||||
### 7.2 Customer Delay & Intention (CID) Codes
|
||||
| Reason Code | Description | Action |
|
||||
| :--- | :--- | :--- |
|
||||
| `CID_CUST_UNAVAILABLE` | Customer not available at pickup address. | Reschedule for next business day. |
|
||||
| `CID_ITEM_NOT_PACKED` | Customer item not packed or ready for handoff. | Reschedule pickup window. |
|
||||
| `CID_CUST_REFUSED` | Customer changed mind and refused to hand over item. | Trigger cancellation workflow. |
|
||||
|
||||
### 7.3 On-Hold & Unreachable Remark Codes
|
||||
| Reason Code | Description | Action |
|
||||
| :--- | :--- | :--- |
|
||||
| `NC_PHONE_UNREACHABLE` | Rider unable to reach customer phone. | Trigger automated SMS/WhatsApp call-back link. |
|
||||
| `NC_ADDRESS_INCOMPLETE`| Customer address untraceable by rider. | Request customer address update via client UI. |
|
||||
|
||||
---
|
||||
|
||||
## 8. Integration Walkthrough & Next Steps
|
||||
|
||||
1. **API Token Setup**: Request Reverse API Token from Shadowfax Integration Team.
|
||||
2. **Webhook Registration**: Configure HTTPS endpoint for receiving reverse state callbacks (`picked_up`, `qc_failed`, `delivered_to_warehouse`).
|
||||
3. **Sandbox Testing**: Create test pickup request $\rightarrow$ Execute simulated QC pass/fail $\rightarrow$ Receive Webhooks $\rightarrow$ Retrieve POD.
|
||||
4. **Go-Live Readiness**: Transition endpoint URL to Production host `https://flash.shadowfax.in`.
|
||||
1894
documentation/sfx_reverse_pickup_spec.apib
Normal file
1894
documentation/sfx_reverse_pickup_spec.apib
Normal file
File diff suppressed because it is too large
Load diff
3025
documentation/sfx_unified_api_spec.apib
Normal file
3025
documentation/sfx_unified_api_spec.apib
Normal file
File diff suppressed because it is too large
Load diff
22
manage.py
Executable file
22
manage.py
Executable 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
4
requirements.txt
Normal 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
0
shipping/__init__.py
Normal file
BIN
shipping/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
shipping/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
shipping/__pycache__/admin.cpython-312.pyc
Normal file
BIN
shipping/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
shipping/__pycache__/apps.cpython-312.pyc
Normal file
BIN
shipping/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
shipping/__pycache__/models.cpython-312.pyc
Normal file
BIN
shipping/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
shipping/__pycache__/sfx_client.cpython-312.pyc
Normal file
BIN
shipping/__pycache__/sfx_client.cpython-312.pyc
Normal file
Binary file not shown.
BIN
shipping/__pycache__/urls.cpython-312.pyc
Normal file
BIN
shipping/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
shipping/__pycache__/views.cpython-312.pyc
Normal file
BIN
shipping/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
3
shipping/admin.py
Normal file
3
shipping/admin.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
5
shipping/apps.py
Normal file
5
shipping/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ShippingConfig(AppConfig):
|
||||
name = 'shipping'
|
||||
35
shipping/migrations/0001_initial.py
Normal file
35
shipping/migrations/0001_initial.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
shipping/migrations/__init__.py
Normal file
0
shipping/migrations/__init__.py
Normal file
BIN
shipping/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
BIN
shipping/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
Binary file not shown.
BIN
shipping/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
shipping/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
33
shipping/models.py
Normal file
33
shipping/models.py
Normal 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
81
shipping/sfx_client.py
Normal 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
|
||||
}
|
||||
294
shipping/static/css/style.css
Normal file
294
shipping/static/css/style.css
Normal 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;
|
||||
}
|
||||
44
shipping/templates/shipping/base.html
Normal file
44
shipping/templates/shipping/base.html
Normal 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>
|
||||
330
shipping/templates/shipping/forward.html
Normal file
330
shipping/templates/shipping/forward.html
Normal 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 %}
|
||||
183
shipping/templates/shipping/logs.html
Normal file
183
shipping/templates/shipping/logs.html
Normal 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;">×</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 %}
|
||||
266
shipping/templates/shipping/reverse.html
Normal file
266
shipping/templates/shipping/reverse.html
Normal 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 %}
|
||||
176
shipping/templates/shipping/webhooks.html
Normal file
176
shipping/templates/shipping/webhooks.html
Normal 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;">×</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
3
shipping/tests.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
20
shipping/urls.py
Normal file
20
shipping/urls.py
Normal 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
650
shipping/views.py
Normal 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})
|
||||
21
shippingservice-beta.service
Normal file
21
shippingservice-beta.service
Normal 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
|
||||
0
shippingservice_project/__init__.py
Normal file
0
shippingservice_project/__init__.py
Normal file
BIN
shippingservice_project/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
shippingservice_project/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
shippingservice_project/__pycache__/settings.cpython-312.pyc
Normal file
BIN
shippingservice_project/__pycache__/settings.cpython-312.pyc
Normal file
Binary file not shown.
BIN
shippingservice_project/__pycache__/urls.cpython-312.pyc
Normal file
BIN
shippingservice_project/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
shippingservice_project/__pycache__/wsgi.cpython-312.pyc
Normal file
BIN
shippingservice_project/__pycache__/wsgi.cpython-312.pyc
Normal file
Binary file not shown.
16
shippingservice_project/asgi.py
Normal file
16
shippingservice_project/asgi.py
Normal 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()
|
||||
153
shippingservice_project/settings.py
Normal file
153
shippingservice_project/settings.py
Normal 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',
|
||||
},
|
||||
}
|
||||
7
shippingservice_project/urls.py
Normal file
7
shippingservice_project/urls.py
Normal 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')),
|
||||
]
|
||||
16
shippingservice_project/wsgi.py
Normal file
16
shippingservice_project/wsgi.py
Normal 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()
|
||||
Loading…
Reference in a new issue