Skip to content

Commit a3fa165

Browse files
feat: implement Shopify order webhooks and order history (#854)
E-Commerce Completion: - Implemented Shopify order creation webhook handler - Added HMAC signature verification for webhook security - Created Order and OrderItem database models - Built order history page for authenticated users - Added API endpoint to fetch user orders Webhook Features: - Verifies Shopify webhook authenticity via HMAC - Stores order details in database - Links orders to VWC users by email - Prevents duplicate order entries - Handles order items with full product details Order History Page: - Displays all user orders (matched by email) - Shows order status badges (paid, pending, etc.) - Displays fulfillment status - Lists all items per order with quantities and prices - Formatted dates and currency - Empty state with link to store - Requires authentication Database Schema: - Order model: tracks Shopify orders with status - OrderItem model: stores individual line items - Linked to User model by email matching - Timestamps for order creation and updates API Endpoints: - POST /api/shopify/webhooks/orders/create - Webhook receiver - GET /api/orders - Fetch user's order history Documentation: - Created ECOMMERCE_TESTING.md with test checklist - Updated .env.example with webhook configuration - Documented all required environment variables Environment Variables: - SHOPIFY_API_SECRET - For webhook HMAC verification - SHOPIFY_ADMIN_ACCESS_TOKEN - Optional for admin API - Supports multiple secret key naming conventions Phase 2 E-Commerce Status: ✅ Cart to checkout flow (already working) ✅ Product variant selection (already working) ✅ Order confirmation webhook (NEW) ✅ Order history page (NEW)
1 parent dab1b81 commit a3fa165

6 files changed

Lines changed: 802 additions & 1 deletion

File tree

.env.example

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
# For production, use PostgreSQL (Vercel Postgres, Railway, Supabase, etc.):
55
DATABASE_URL="postgresql://username:password@hostname:port/database"
66

7+
# LMS Development Access - Set to 'true' to bypass auth in development ONLY
8+
# WARNING: Never enable in production!
9+
LMS_DEV_BYPASS=false
10+
711
# NextAuth.js
812
NEXTAUTH_SECRET="your-secret-key-here"
913
NEXTAUTH_URL="http://localhost:3000"
@@ -12,6 +16,14 @@ NEXTAUTH_URL="http://localhost:3000"
1216
GITHUB_CLIENT_ID="your-github-client-id"
1317
GITHUB_CLIENT_SECRET="your-github-client-secret"
1418

19+
# GitHub API Access (for fetching org data, repos, etc.)
20+
GITHUB_PAT="your-github-personal-access-token"
21+
GITHUB_TOKEN="your-github-token"
22+
GITHUB_ORG="Vets-Who-Code"
23+
24+
# Google Maps API (for location features)
25+
NEXT_PUBLIC_GOOGLE_MAPS_KEY="your-google-maps-api-key"
26+
1527
# Optional: Other OAuth providers
1628
# GOOGLE_CLIENT_ID="your-google-client-id"
1729
# GOOGLE_CLIENT_SECRET="your-google-client-secret"
@@ -35,11 +47,19 @@ OPENAI_API_KEY="your-openai-api-key"
3547
PHI3_ENDPOINT="your-phi3-endpoint"
3648
PHI3_API_KEY="your-phi3-api-key"
3749

38-
# Shopify Storefront API
50+
# Shopify Storefront API (for browsing products/cart)
3951
SHOPIFY_STORE_DOMAIN="your-store.myshopify.com"
4052
SHOPIFY_STOREFRONT_ACCESS_TOKEN="your-storefront-access-token"
53+
54+
# Shopify Admin API (optional - for making API calls to Shopify)
4155
SHOPIFY_ADMIN_ACCESS_TOKEN="your-admin-access-token"
4256

57+
# Shopify Webhook/API Secrets (REQUIRED for order webhooks - the API Secret Key from API credentials)
58+
# The webhook handler will check for any of these names:
59+
SHOPIFY_WEBHOOK_SECRET="your-webhook-secret" # Primary option
60+
SHOPIFY_API_SECRET="your-api-secret-key" # Alternative name
61+
SHOPIFY_CLIENT_SECRET="your-client-secret" # Alternative name
62+
4363
# Cloudinary (for image uploads and management)
4464
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME="your-cloud-name"
4565
CLOUDINARY_API_KEY="your-api-key"

docs/ECOMMERCE_TESTING.md

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# E-Commerce Testing Checklist
2+
3+
## Test Environment Setup
4+
5+
### Required Environment Variables
6+
```bash
7+
SHOPIFY_STORE_DOMAIN="your-store.myshopify.com"
8+
SHOPIFY_STOREFRONT_ACCESS_TOKEN="your-storefront-token"
9+
SHOPIFY_ADMIN_ACCESS_TOKEN="your-admin-token"
10+
```
11+
12+
## 1. Cart to Checkout Flow ✅
13+
14+
### Test Steps:
15+
1. Navigate to `/store`
16+
2. Click on a product
17+
3. Select variant options (if applicable)
18+
4. Adjust quantity
19+
5. Click "Add to Cart"
20+
6. Cart slide-out should open
21+
7. Verify item appears in cart with correct:
22+
- Product name
23+
- Variant details
24+
- Price
25+
- Quantity
26+
8. Adjust quantity in cart
27+
9. Click "Proceed to Checkout"
28+
10. Should redirect to Shopify checkout page
29+
30+
### Expected Behavior:
31+
- ✅ Cart persists across page refreshes (localStorage)
32+
- ✅ Correct pricing calculations
33+
- ✅ Checkout URL redirects to Shopify
34+
- ✅ Can update quantities
35+
- ✅ Can remove items
36+
37+
### Code Locations:
38+
- Store page: `src/pages/store/index.tsx`
39+
- Product detail: `src/pages/store/products/[handle].tsx`
40+
- Cart component: `src/components/shopping-cart/shopping-cart.tsx`
41+
- Cart hook: `src/hooks/useCart.ts`
42+
- Shopify API: `src/lib/shopify.ts`
43+
44+
## 2. Product Variant Selection ✅
45+
46+
### Test Steps:
47+
1. Find a product with variants (Size, Color, etc.)
48+
2. Click on different variant options
49+
3. Verify:
50+
- Price updates if variant has different price
51+
- Image changes if variant has specific image
52+
- "Add to Cart" uses correct variant ID
53+
- Selected variant is highlighted
54+
55+
### Expected Behavior:
56+
- ✅ Options are displayed as buttons
57+
- ✅ Selecting option finds matching variant
58+
- ✅ Visual feedback for selected option
59+
- ✅ Image gallery updates
60+
- ✅ Correct variant added to cart
61+
62+
### Code Location:
63+
- Variant selection: `src/pages/store/products/[handle].tsx:68-93`
64+
65+
## 3. Order Confirmation Webhook ❌ TO IMPLEMENT
66+
67+
### Requirements:
68+
- Shopify webhook endpoint at `/api/shopify/webhooks/orders/create`
69+
- Verify webhook signature (HMAC)
70+
- Store order details in database
71+
- Send confirmation email to customer (optional)
72+
73+
### Webhook Setup in Shopify:
74+
1. Go to Shopify Admin → Settings → Notifications → Webhooks
75+
2. Create webhook:
76+
- Event: Order creation
77+
- Format: JSON
78+
- URL: `https://your-domain.com/api/shopify/webhooks/orders/create`
79+
- Version: 2024-01 or latest
80+
81+
### Test Steps:
82+
1. Complete a test order on Shopify checkout
83+
2. Verify webhook is received at endpoint
84+
3. Check database for order record
85+
4. Verify order details are correct
86+
87+
### Expected Data:
88+
```json
89+
{
90+
"id": 1234567890,
91+
"email": "customer@example.com",
92+
"total_price": "29.99",
93+
"currency": "USD",
94+
"line_items": [...],
95+
"created_at": "2024-01-02T00:00:00Z",
96+
"customer": {...}
97+
}
98+
```
99+
100+
## 4. Order History Page ❌ TO IMPLEMENT
101+
102+
### Requirements:
103+
- Page at `/orders` or `/store/orders`
104+
- Fetch user's orders from database or Shopify
105+
- Display order list with:
106+
- Order number
107+
- Date
108+
- Items
109+
- Total amount
110+
- Order status
111+
- Tracking info (if available)
112+
113+
### Test Steps:
114+
1. Login as user
115+
2. Navigate to `/orders`
116+
3. Verify orders are displayed
117+
4. Click on an order to see details
118+
5. Check tracking link (if available)
119+
120+
### Expected Behavior:
121+
- Shows all user's orders
122+
- Most recent orders first
123+
- Can view order details
124+
- Shows order status (pending, fulfilled, etc.)
125+
- Empty state if no orders
126+
127+
## Browser Testing Matrix
128+
129+
Test on:
130+
- ✅ Chrome (latest)
131+
- ✅ Firefox (latest)
132+
- ✅ Safari (latest)
133+
- ✅ Mobile Safari (iOS)
134+
- ✅ Chrome Mobile (Android)
135+
136+
## Performance Checks
137+
138+
- [ ] Cart operations complete in < 1s
139+
- [ ] Product page loads in < 2s
140+
- [ ] Images are optimized
141+
- [ ] No console errors
142+
- [ ] Proper loading states
143+
144+
## Security Checks
145+
146+
- [ ] Webhook signature verification
147+
- [ ] CORS configured correctly
148+
- [ ] No sensitive data in frontend
149+
- [ ] API keys not exposed
150+
- [ ] HTTPS only for checkout
151+
152+
## Accessibility Checks
153+
154+
- [ ] Keyboard navigation works
155+
- [ ] Screen reader friendly
156+
- [ ] ARIA labels on interactive elements
157+
- [ ] Focus states visible
158+
- [ ] Color contrast meets WCAG AA
159+
160+
## Notes
161+
162+
- Cart uses Shopify Storefront API (read-only, safe for frontend)
163+
- Checkout happens on Shopify (PCI compliant, secure)
164+
- Webhooks use Admin API (requires HMAC verification)
165+
- Orders stored locally for history/analytics
166+
167+
## Known Issues
168+
169+
None currently
170+
171+
## Future Enhancements
172+
173+
- [ ] Wishlist functionality
174+
- [ ] Product reviews
175+
- [ ] Related products
176+
- [ ] Recently viewed products
177+
- [ ] Abandoned cart recovery
178+
- [ ] Discount code validation

prisma/schema.prisma

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ model User {
6161
bookmarks Bookmark[]
6262
notes Note[]
6363
64+
// E-commerce relationships
65+
orders Order[]
66+
6467
createdAt DateTime @default(now())
6568
updatedAt DateTime @updatedAt
6669
}
@@ -391,4 +394,63 @@ model Note {
391394
// Relationships
392395
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
393396
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
397+
}
398+
399+
// E-Commerce Models - Shopify order tracking
400+
401+
model Order {
402+
id String @id @default(cuid())
403+
shopifyId String @unique // Shopify order ID
404+
orderNumber String // Human-readable order number
405+
406+
// Customer info
407+
userId String? // Linked VWC user (if authenticated)
408+
customerEmail String
409+
customerName String?
410+
411+
// Order details
412+
totalPrice String // Total price (as string to maintain precision)
413+
currency String @default("USD")
414+
financialStatus String // paid, pending, refunded, etc.
415+
fulfillmentStatus String? // fulfilled, partial, unfulfilled, etc.
416+
417+
// Order metadata
418+
tags String[] // Shopify tags
419+
note String? // Order notes
420+
421+
// Timestamps
422+
orderCreatedAt DateTime // When order was created in Shopify
423+
createdAt DateTime @default(now())
424+
updatedAt DateTime @updatedAt
425+
426+
// Relationships
427+
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
428+
items OrderItem[]
429+
}
430+
431+
model OrderItem {
432+
id String @id @default(cuid())
433+
orderId String
434+
435+
// Product details
436+
shopifyProductId String
437+
shopifyVariantId String
438+
productTitle String
439+
variantTitle String?
440+
441+
// Pricing
442+
quantity Int
443+
price String // Price per item
444+
totalPrice String // quantity * price
445+
446+
// SKU and metadata
447+
sku String?
448+
vendor String?
449+
imageUrl String?
450+
451+
// Timestamps
452+
createdAt DateTime @default(now())
453+
454+
// Relationships
455+
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
394456
}

src/pages/api/orders/index.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { NextApiResponse } from 'next';
2+
import { requireAuth, AuthenticatedRequest } from '@/lib/rbac';
3+
import prisma from '@/lib/prisma';
4+
5+
/**
6+
* GET /api/orders
7+
*
8+
* Fetch all orders for the authenticated user
9+
*
10+
* Response:
11+
* {
12+
* orders: [
13+
* {
14+
* id: string,
15+
* shopifyId: string,
16+
* orderNumber: string,
17+
* totalPrice: string,
18+
* currency: string,
19+
* financialStatus: string,
20+
* fulfillmentStatus: string,
21+
* orderCreatedAt: Date,
22+
* items: OrderItem[]
23+
* }
24+
* ]
25+
* }
26+
*/
27+
export default requireAuth(async (req: AuthenticatedRequest, res: NextApiResponse) => {
28+
if (req.method !== 'GET') {
29+
return res.status(405).json({ error: 'Method not allowed' });
30+
}
31+
32+
try {
33+
const userId = req.user!.id;
34+
const userEmail = req.user!.email;
35+
36+
// Fetch orders for this user
37+
// Match by userId OR by email (for orders before user logged in)
38+
const orders = await prisma.order.findMany({
39+
where: {
40+
OR: [
41+
{ userId },
42+
{ customerEmail: userEmail },
43+
],
44+
},
45+
include: {
46+
items: {
47+
orderBy: {
48+
createdAt: 'asc',
49+
},
50+
},
51+
},
52+
orderBy: {
53+
orderCreatedAt: 'desc', // Most recent first
54+
},
55+
});
56+
57+
return res.status(200).json({ orders });
58+
} catch (error) {
59+
console.error('Error fetching orders:', error);
60+
return res.status(500).json({ error: 'Failed to fetch orders' });
61+
}
62+
});

0 commit comments

Comments
 (0)