Feature: Add WhatsApp notifications for collection-ready orders

- Implement WhatsApp Business API integration via Meta Cloud API
- Add phoneNumber field to Order model for storing customer phone numbers
- Update admin order detail page with phone number input for collection orders
- Send WhatsApp notification when admin marks order as ready for collection
- Add WhatsApp service module with phone number formatting and message sending
- Create database migrations for collection-related fields
- Add WhatsApp credentials to .env configuration

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Andymick
2026-07-19 09:29:09 +01:00
co-authored by Claude Haiku 4.5
parent a105fdf660
commit 36cc6dddff
26 changed files with 553 additions and 80 deletions
+32 -4
View File
@@ -56,11 +56,34 @@ export async function POST(req: Request) {
cleanupPendingOrders().catch((err) => console.error('Cleanup error:', err));
const body = await req.json();
console.log('Checkout request received with items:', body.items?.length, 'customerId:', body.customerId);
console.log('Checkout request received with items:', body.items?.length, 'customerId:', body.customerId, 'isCollection:', body.isCollection);
const items: CheckoutCartItem[] = body.items ?? [];
const guest: GuestInfo | undefined = body.guest;
const shippingCountry: string = body.shippingCountry ?? 'GB';
const shippingService: string | undefined = body.shippingService;
const isCollection: boolean = body.isCollection ?? false;
const customer = await getCurrentCustomer();
const checkoutEmail = guest?.email ?? customer?.email;
// Delete any other recent PENDING orders for this email to avoid duplicates
if (checkoutEmail) {
try {
const deletedCount = await prisma.order.deleteMany({
where: {
email: checkoutEmail,
status: 'PENDING',
createdAt: {
gte: new Date(Date.now() - 2 * 60 * 60 * 1000), // Within last 2 hours
},
},
});
if (deletedCount.count > 0) {
console.log(`Cleaned up ${deletedCount.count} duplicate pending order(s) for ${checkoutEmail}`);
}
} catch (err) {
console.error('Error deleting duplicate pending orders:', err);
}
}
if (items.length === 0) {
return NextResponse.json({ error: 'Your bag is empty.' }, { status: 400 });
@@ -95,11 +118,15 @@ export async function POST(req: Request) {
const subtotal = pricedItems.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
// Calculate shipping cost
// Calculate shipping cost (skip for collection orders)
let shippingCost = 0;
let selectedShippingService = shippingService;
try {
if (isCollection) {
console.log('Collection order - no shipping cost');
selectedShippingService = null;
} else {
try {
// Calculate total weight from cart items
let totalWeightGrams = 0;
for (const item of items) {
@@ -138,9 +165,9 @@ export async function POST(req: Request) {
// Default shipping cost if calculation fails
shippingCost = 500; // £5.00
}
}
const total = subtotal + shippingCost;
const customer = await getCurrentCustomer();
// Save the order up front, before redirecting to Stripe — this is what makes
// the design files (and the order itself) retrievable afterward, whether or
@@ -156,6 +183,7 @@ export async function POST(req: Request) {
email: guest?.email ?? customer?.email,
guestName: guest?.name,
guestPhone: guest?.phone,
isCollectionPickup,
items: {
create: pricedItems.map((i) => ({
productId: i.productId,