Skip to content

Commit dcd114c

Browse files
Saad NadeemSaad Nadeem
authored andcommitted
application-ready
0 parents  commit dcd114c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+9595
-0
lines changed

.DS_Store

6 KB
Binary file not shown.

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules/
2+
.next/
3+
*.log
4+
.env.local
5+
__pycache__/

backend/.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
SHIPPO_API_KEY=shippo_test_b0d2ab85b1ba800ca8cf2a58cc2e404eaf9f8a0d
2+
INTERNAL_API_KEY=myTotalySecretKey

backend/Dockerfile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Use official Python image
2+
FROM python:3.11-slim
3+
4+
# Set working directory
5+
WORKDIR /app
6+
7+
# Copy requirements and install
8+
COPY requirements.txt .
9+
RUN pip install --no-cache-dir -r requirements.txt
10+
11+
# Copy backend source code
12+
COPY . .
13+
14+
# Expose FastAPI port
15+
EXPOSE 8000
16+
17+
# Run the FastAPI app with Uvicorn
18+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

backend/main.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
from fastapi import FastAPI, HTTPException, Depends, status
2+
from fastapi.security import APIKeyHeader
3+
from shippo import Shippo
4+
from shippo.models import components
5+
from shippo.models.errors import SDKError
6+
from pydantic import BaseModel, Field
7+
from fastapi.middleware.cors import CORSMiddleware
8+
import os
9+
import logging
10+
from dotenv import load_dotenv
11+
from typing import Optional
12+
import re
13+
14+
load_dotenv()
15+
16+
logging.basicConfig(
17+
level=logging.INFO,
18+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
19+
handlers=[logging.StreamHandler()]
20+
)
21+
logger = logging.getLogger(__name__)
22+
23+
app = FastAPI(
24+
title="Shipping API",
25+
description="API for creating and tracking shipments using Shippo",
26+
version="1.0.0"
27+
)
28+
29+
SHIPPO_API_KEY = os.getenv("SHIPPO_API_KEY")
30+
if not SHIPPO_API_KEY:
31+
logger.error("SHIPPO_API_KEY not set in environment variables")
32+
raise RuntimeError("SHIPPO_API_KEY is required")
33+
34+
shippo_sdk = Shippo(api_key_header=SHIPPO_API_KEY)
35+
36+
MOCK_TRACKING_NUMBER = "SHIPPO_TRANSIT"
37+
38+
app.add_middleware(
39+
CORSMiddleware,
40+
allow_origins=["http://localhost:3000"],
41+
allow_credentials=True,
42+
allow_methods=["*"],
43+
allow_headers=["*"],
44+
)
45+
46+
class AddressRequest(BaseModel):
47+
name: str = Field(..., min_length=1, max_length=100)
48+
street1: str = Field(..., min_length=1, max_length=100)
49+
street2: Optional[str] = Field(None, max_length=100)
50+
city: str = Field(..., min_length=1, max_length=100)
51+
state: str = Field(..., min_length=2, max_length=2)
52+
zip: str = Field(..., pattern=r"^\d{5}(-\d{4})?$")
53+
country: str = Field(..., min_length=2, max_length=2)
54+
phone: Optional[str] = Field(None, pattern=r"^\+?\d{10,15}$")
55+
email: Optional[str] = Field(None, max_length=100)
56+
57+
class ParcelRequest(BaseModel):
58+
length: str = Field(..., pattern=r"^\d+(\.\d+)?$")
59+
width: str = Field(..., pattern=r"^\d+(\.\d+)?$")
60+
height: str = Field(..., pattern=r"^\d+(\.\d+)?$")
61+
distance_unit: components.DistanceUnitEnum # Updated from DistanceUnitEnum
62+
weight: str = Field(..., pattern=r"^\d+(\.\d+)?$")
63+
mass_unit: components.WeightUnitEnum # Updated from WeightUnitEnum
64+
65+
class ShipmentRequest(BaseModel):
66+
address_from: AddressRequest
67+
address_to: AddressRequest
68+
parcels: list[ParcelRequest]
69+
70+
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
71+
72+
def require_api_key(api_key: str = Depends(api_key_header)):
73+
if not api_key or api_key != os.getenv("INTERNAL_API_KEY"):
74+
raise HTTPException(
75+
status_code=status.HTTP_403_FORBIDDEN,
76+
detail="Invalid or missing API key"
77+
)
78+
return api_key
79+
80+
@app.post("/create-order", response_model=dict, dependencies=[Depends(require_api_key)])
81+
def create_order(request: ShipmentRequest):
82+
try:
83+
address_from = components.AddressCreateRequest(
84+
name=request.address_from.name,
85+
street1=request.address_from.street1,
86+
street2=request.address_from.street2,
87+
city=request.address_from.city,
88+
state=request.address_from.state,
89+
zip=request.address_from.zip,
90+
country=request.address_from.country,
91+
phone=request.address_from.phone,
92+
email=request.address_from.email
93+
)
94+
95+
address_to = components.AddressCreateRequest(
96+
name=request.address_to.name,
97+
street1=request.address_to.street1,
98+
street2=request.address_to.street2,
99+
city=request.address_to.city,
100+
state=request.address_to.state,
101+
zip=request.address_to.zip,
102+
country=request.address_to.country,
103+
phone=request.address_to.phone,
104+
email=request.address_to.email
105+
)
106+
107+
parcel_data = request.parcels[0]
108+
parcel = components.ParcelCreateRequest(
109+
length=parcel_data.length,
110+
width=parcel_data.width,
111+
height=parcel_data.height,
112+
distance_unit=parcel_data.distance_unit,
113+
weight=parcel_data.weight,
114+
mass_unit=parcel_data.mass_unit
115+
)
116+
117+
logger.info("Creating shipment from %s to %s", address_from.city, address_to.city)
118+
shipment = shippo_sdk.shipments.create(
119+
components.ShipmentCreateRequest(
120+
address_from=address_from,
121+
address_to=address_to,
122+
parcels=[parcel],
123+
async_=False
124+
)
125+
)
126+
127+
if not shipment.rates:
128+
logger.warning("No rates available for shipment %s", shipment.object_id)
129+
raise HTTPException(status_code=400, detail="No rates available for this shipment")
130+
131+
rates = [f"{r.provider}: {r.amount} {r.currency}" for r in shipment.rates]
132+
logger.info("Rates available: %s", rates)
133+
134+
rate = next((r for r in shipment.rates if r.provider == "USPS"), shipment.rates[0])
135+
136+
logger.info("Creating transaction with rate %s", rate.object_id)
137+
transaction = shippo_sdk.transactions.create(
138+
components.TransactionCreateRequest(
139+
rate=rate.object_id,
140+
label_file_type="PDF",
141+
async_=False
142+
)
143+
)
144+
145+
response_data = {
146+
"shipment_object_id": shipment.object_id,
147+
"transaction_status": transaction.status,
148+
}
149+
150+
if transaction.status == "SUCCESS":
151+
response_data["tracking_number"] = transaction.tracking_number
152+
response_data["label_url"] = transaction.label_url
153+
logger.info("Transaction successful. Tracking: %s, Label: %s",
154+
transaction.tracking_number, transaction.label_url)
155+
else:
156+
messages = [msg.to_dict() for msg in transaction.messages] if transaction.messages else []
157+
logger.error("Transaction failed: %s", messages)
158+
response_data["error"] = "Transaction failed"
159+
response_data["messages"] = messages
160+
raise HTTPException(status_code=400, detail={"error": "Transaction failed", "messages": messages})
161+
162+
return response_data
163+
164+
except SDKError as e:
165+
logger.error("Shippo API error: %s", str(e))
166+
raise HTTPException(status_code=500, detail=f"API Error: {str(e)}")
167+
except HTTPException as e:
168+
raise e
169+
except Exception as e:
170+
logger.error("Unexpected error: %s", str(e))
171+
raise HTTPException(status_code=500, detail="Internal server error")
172+
173+
@app.get("/track/{tracking_number}", response_model=dict, dependencies=[Depends(require_api_key)])
174+
def track_order(tracking_number: str):
175+
if not re.match(r"^[A-Za-z0-9_-]+$", tracking_number):
176+
logger.warning("Invalid tracking number format: %s", tracking_number)
177+
raise HTTPException(status_code=400, detail="Invalid tracking number format")
178+
179+
try:
180+
logger.info("Tracking request for %s (using mock %s)", tracking_number, MOCK_TRACKING_NUMBER)
181+
tracking = shippo_sdk.tracking_status.get(carrier="shippo", tracking_number=MOCK_TRACKING_NUMBER)
182+
183+
response = {
184+
"carrier": tracking.carrier,
185+
"tracking_number": tracking_number,
186+
"status": tracking.tracking_status.status,
187+
"history": [
188+
{
189+
"status": event.status,
190+
"date": event.status_date,
191+
"details": event.status_details,
192+
"location": event.location or "Unknown"
193+
} for event in tracking.tracking_history
194+
]
195+
}
196+
logger.info("Tracking response for %s: %s", tracking_number, response["status"])
197+
return response
198+
199+
except SDKError as e:
200+
logger.error("Tracking error for %s: %s", tracking_number, str(e))
201+
raise HTTPException(status_code=500, detail=f"Tracking error: {str(e)}")
202+
except Exception as e:
203+
logger.error("Unexpected tracking error for %s: %s", tracking_number, str(e))
204+
raise HTTPException(status_code=500, detail="Internal server error")
205+
206+
if __name__ == "__main__":
207+
import uvicorn
208+
uvicorn.run(app, host="0.0.0.0", port=8000)

backend/requirements.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
fastapi==0.110.0
2+
uvicorn==0.27.1
3+
requests==2.31.0
4+
shippo>=3.1.0 # Updated to allow latest compatible version
5+
python-dotenv==1.0.1
6+
pydantic==2.6.0

docker-compose.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
version: '3.8'
2+
3+
services:
4+
frontend:
5+
build:
6+
context: ./frontend
7+
dockerfile: Dockerfile
8+
ports:
9+
- "3000:3000"
10+
environment:
11+
- NEXT_PUBLIC_API_URL=http://backend:8000
12+
- NEXT_PUBLIC_INTERNAL_API_KEY=myTotalySecretKey
13+
depends_on:
14+
- backend
15+
restart: always # Added restart policy
16+
17+
backend:
18+
build:
19+
context: ./backend
20+
dockerfile: Dockerfile
21+
ports:
22+
- "8000:8000"
23+
environment:
24+
- INTERNAL_API_KEY=myTotalySecretKey
25+
- SHIPPO_API_KEY=shippo_test_b0d2ab85b1ba800ca8cf2a58cc2e404eaf9f8a0d
26+
restart: always # Added restart policy

frontend/.DS_Store

8 KB
Binary file not shown.

frontend/Dockerfile

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Use official Node.js LTS image
2+
FROM node:20-alpine AS builder
3+
4+
# Set working directory
5+
WORKDIR /app
6+
7+
# Copy package files and install dependencies
8+
COPY package.json package-lock.json ./
9+
RUN npm install --frozen-lockfile
10+
11+
# Copy source code
12+
COPY . .
13+
14+
# Build the Next.js app
15+
RUN npm run build
16+
17+
# Production stage
18+
FROM node:20-alpine
19+
WORKDIR /app
20+
COPY --from=builder /app/.next ./.next
21+
COPY --from=builder /app/public ./public
22+
COPY --from=builder /app/package.json ./package.json
23+
COPY --from=builder /app/node_modules ./node_modules
24+
25+
# Expose port (default for Next.js)
26+
EXPOSE 3000
27+
28+
# Start the app
29+
CMD ["npm", "start"]

frontend/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2+
3+
## Getting Started
4+
5+
First, run the development server:
6+
7+
```bash
8+
npm run dev
9+
# or
10+
yarn dev
11+
# or
12+
pnpm dev
13+
# or
14+
bun dev
15+
```
16+
17+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18+
19+
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20+
21+
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22+
23+
## Learn More
24+
25+
To learn more about Next.js, take a look at the following resources:
26+
27+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29+
30+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31+
32+
## Deploy on Vercel
33+
34+
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35+
36+
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

0 commit comments

Comments
 (0)