diff --git a/.claude/settings.local.json b/.claude/settings.local.json index d10072e..dd18139 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -76,7 +76,8 @@ "Bash(git commit -m 'Fix: Ensure cart is completely cleared after payment success *)", "Bash(git commit -m 'Fix: Update order status to PAID on checkout success page *)", "Bash(git commit -m 'Improve: Cart clearing on success page with better state management *)", - "Bash(git commit -m 'Feature: Add order tracking and shipping status updates *)" + "Bash(git commit -m 'Feature: Add order tracking and shipping status updates *)", + "Bash(git commit -m 'Feature: Royal Mail tracking integration \\(hybrid approach\\) *)" ] } } diff --git a/src/app/admin/orders/[id]/page.tsx b/src/app/admin/orders/[id]/page.tsx index cb1f20c..d0ec630 100644 --- a/src/app/admin/orders/[id]/page.tsx +++ b/src/app/admin/orders/[id]/page.tsx @@ -1,6 +1,7 @@ import { notFound } from 'next/navigation'; import { prisma } from '@/lib/prisma'; import AdminNav from '@/components/AdminNav'; +import SyncRoyalMailButton from '@/components/SyncRoyalMailButton'; import { archiveOrder, unarchiveOrder, updateOrderTracking } from '../actions'; function formatPrice(cents: number) { @@ -133,13 +134,29 @@ export default async function OrderDetailPage({ params }: { params: { id: string - +
+ +
+ + {order.trackingNumber && ( +
+

Royal Mail Integration

+ { + // In a real app, we'd refresh the page or update state + console.log('Tracking synced successfully'); + }} + /> +
+ )}
diff --git a/src/app/api/orders/[id]/sync-tracking/route.ts b/src/app/api/orders/[id]/sync-tracking/route.ts new file mode 100644 index 0000000..26579aa --- /dev/null +++ b/src/app/api/orders/[id]/sync-tracking/route.ts @@ -0,0 +1,54 @@ +import { NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { fetchRoyalMailTracking, mapTrackingStatusToOrderStatus } from '@/lib/royalmail'; + +export async function POST( + req: Request, + { params }: { params: { id: string } } +) { + try { + const order = await prisma.order.findUnique({ + where: { id: params.id }, + }); + + if (!order) { + return NextResponse.json({ error: 'Order not found' }, { status: 404 }); + } + + if (!order.trackingNumber) { + return NextResponse.json({ error: 'No tracking number set for this order' }, { status: 400 }); + } + + // Fetch tracking data from Royal Mail + const trackingData = await fetchRoyalMailTracking(order.trackingNumber); + + if (!trackingData) { + return NextResponse.json({ error: 'Could not fetch tracking data from Royal Mail' }, { status: 400 }); + } + + // Map Royal Mail status to our order status + const newStatus = mapTrackingStatusToOrderStatus(trackingData.status); + + // Update order with new status + const updated = await prisma.order.update({ + where: { id: params.id }, + data: { + status: newStatus, + }, + }); + + console.log(`✓ Order ${params.id} tracking synced: ${trackingData.status} → ${newStatus}`); + + return NextResponse.json({ + success: true, + trackingData, + orderStatus: newStatus, + }); + } catch (err) { + console.error('Error syncing tracking:', err); + return NextResponse.json( + { error: 'Failed to sync tracking data' }, + { status: 500 } + ); + } +} diff --git a/src/components/SyncRoyalMailButton.tsx b/src/components/SyncRoyalMailButton.tsx new file mode 100644 index 0000000..d896a3c --- /dev/null +++ b/src/components/SyncRoyalMailButton.tsx @@ -0,0 +1,71 @@ +'use client'; + +import { useState } from 'react'; + +interface SyncRoyalMailButtonProps { + orderId: string; + trackingNumber?: string | null; + onSuccess?: () => void; +} + +export default function SyncRoyalMailButton({ orderId, trackingNumber, onSuccess }: SyncRoyalMailButtonProps) { + const [loading, setLoading] = useState(false); + const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + + const handleSync = async () => { + if (!trackingNumber) { + setMessage({ type: 'error', text: 'Please enter a tracking number first' }); + return; + } + + setLoading(true); + setMessage(null); + + try { + const res = await fetch(`/api/orders/${orderId}/sync-tracking`, { + method: 'POST', + }); + + const data = await res.json(); + + if (!res.ok) { + setMessage({ type: 'error', text: data.error || 'Failed to sync tracking' }); + return; + } + + setMessage({ + type: 'success', + text: `Tracking synced! Status: ${data.trackingData.status}`, + }); + + onSuccess?.(); + } catch (err) { + setMessage({ type: 'error', text: 'Error syncing with Royal Mail' }); + } finally { + setLoading(false); + } + }; + + return ( +
+ + + {message && ( +

+ {message.text} +

+ )} +
+ ); +} diff --git a/src/lib/royalmail.ts b/src/lib/royalmail.ts new file mode 100644 index 0000000..533eced --- /dev/null +++ b/src/lib/royalmail.ts @@ -0,0 +1,115 @@ +// Royal Mail Parcel Tracking API integration +// Uses Royal Mail's public tracking API to get shipment status + +export type RoyalMailTrackingStatus = + | 'Pending' + | 'Collected' + | 'In Transit' + | 'Out for Delivery' + | 'Delivered' + | 'Undeliverable' + | 'Unknown'; + +export interface RoyalMailTrackingEvent { + status: RoyalMailTrackingStatus; + timestamp: Date; + location?: string; + detail?: string; +} + +export interface RoyalMailTrackingData { + trackingNumber: string; + status: RoyalMailTrackingStatus; + events: RoyalMailTrackingEvent[]; + lastUpdate: Date; +} + +/** + * Fetch tracking data from Royal Mail API + * Uses the public Royal Mail tracking service (no authentication required for basic tracking) + */ +export async function fetchRoyalMailTracking(trackingNumber: string): Promise { + try { + // Royal Mail tracking API endpoint + const url = `https://www3.royalmail.com/api/v1/track-and-trace?mailPieces=${trackingNumber}`; + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + }); + + if (!response.ok) { + console.error('Royal Mail API error:', response.status, response.statusText); + return null; + } + + const data = await response.json(); + + // Parse Royal Mail's response format + if (!data.mailPieces || !Array.isArray(data.mailPieces) || data.mailPieces.length === 0) { + console.warn('No tracking data found for:', trackingNumber); + return null; + } + + const mailPiece = data.mailPieces[0]; + + // Extract status and events + const status: RoyalMailTrackingStatus = parseRoyalMailStatus(mailPiece.status); + const events: RoyalMailTrackingEvent[] = (mailPiece.events || []).map((event: any) => ({ + status: parseRoyalMailStatus(event.status), + timestamp: new Date(event.datetime), + location: event.location || event.name, + detail: event.detail, + })); + + return { + trackingNumber: mailPiece.mailPieceId, + status, + events: events.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()), + lastUpdate: new Date(), + }; + } catch (error) { + console.error('Error fetching Royal Mail tracking:', error); + return null; + } +} + +/** + * Map Royal Mail status strings to our internal status format + */ +function parseRoyalMailStatus(royalMailStatus: string): RoyalMailTrackingStatus { + const status = String(royalMailStatus).toLowerCase(); + + if (status.includes('delivered')) return 'Delivered'; + if (status.includes('out for delivery')) return 'Out for Delivery'; + if (status.includes('in transit')) return 'In Transit'; + if (status.includes('collected')) return 'Collected'; + if (status.includes('pending')) return 'Pending'; + if (status.includes('undeliverable')) return 'Undeliverable'; + + return 'Unknown'; +} + +/** + * Convert Royal Mail tracking status to our order status + */ +export function mapTrackingStatusToOrderStatus(trackingStatus: RoyalMailTrackingStatus): string { + switch (trackingStatus) { + case 'Delivered': + return 'DELIVERED'; + case 'Out for Delivery': + return 'SHIPPED'; + case 'In Transit': + return 'SHIPPED'; + case 'Collected': + return 'PRINTED'; + case 'Pending': + return 'PRINTED'; + case 'Undeliverable': + return 'SHIPPED'; // Keep as shipped, admin needs to handle + default: + return 'SHIPPED'; + } +}