File: /var/www/javago-api-updates/src/helper/schedule.js
import GroupCoffeeRun from "../models/group_coffee_run.model.js";
import User from "../models/users.model.js";
import Orders from "../models/orders.model.js";
import moment from "moment/moment.js";
import scheduleJob from "node-schedule";
import { messages } from "../config/response.messages.js";
import firebaseAdmin from "./firebase/firebase_admin.js";
import NotificationList from "../models/notification_list.model.js";
import { Op, where } from "sequelize";
import Cafes from "../models/cafes.model.js";
import notification from "./notification.js";
import { findById } from "../services/auth/auth.services.js";
const schedule = {};
schedule.createSchedule = async function (data) {
try {
const exactTime = moment.unix(data.time);
const scheduleId = Math.floor(Math.random() * 1000);
scheduleJob.scheduleJob(`${scheduleId}`, exactTime.toDate(), async () => {
const usersGroupCoffeeRun = await GroupCoffeeRun.findAll({
attributes: ["id", "group_id", "user_id"],
where: {
request_unique_id: data.coffeeRunId,
user_id: data.sender_id,
},
include: [
{
model: User,
as: "user_data",
attributes: ["id", "fcm_token"],
where: {
notification_status: 1,
},
},
],
});
const scheduledNotifications = usersGroupCoffeeRun.map((e) => {
return {
sender_id: data.sender_id,
receiver_id: e.dataValues.user_data.dataValues.id,
reference_id: data.reference_id,
notification_type: 6,
is_read: 0,
created_at: moment().unix(),
updated_at: moment().unix(),
};
});
if (
data.clickCollectStatus &&
data.clickCollectStatus.toLowerCase() ===
"click and collect is available".toLowerCase()
) {
await NotificationList.bulkCreate(scheduledNotifications);
}
const tokens = usersGroupCoffeeRun
.filter((e) => e && e.dataValues.user_data.dataValues.fcm_token)
.map((e) => e.dataValues.user_data.dataValues.fcm_token);
if (tokens.length == 0) {
console.log("No tokens available");
return;
}
const payload = {
tokens: tokens,
title: messages.schedule_notification_title,
body: messages.schedule_notification_body,
notification_type: `6`,
data: {
group_id: `${usersGroupCoffeeRun[0].dataValues.group_id}`,
notification_type: `6`,
},
};
if (
data.clickCollectStatus &&
data.clickCollectStatus.toLowerCase() ===
"click and collect is available".toLowerCase()
) {
await firebaseAdmin.sendNotification(payload);
}
// ✅ Update orders table to set is_timer_completed = 1 for the coffee run
await Orders.update(
{ is_timer_completed: 1 },
{
where: {
request_unique_id: data.coffeeRunId,
},
}
);
await syncGroupOrdersToSquareGrouped(data.coffeeRunId);
//here get the order of the coffee run and then save to square
let isInitiator = await GroupCoffeeRun.findOne({
where: {
request_unique_id: data.coffeeRunId,
user_id: data.sender_id,
request_created_by: data.sender_id,
},
attributes: ["id", "request_created_by", "order_id"],
});
let userData = await User.findOne({
where: {
id: data.sender_id,
},
attributes: ["id", "name"],
});
console.log("isInitiator", isInitiator);
if (isInitiator) {
const orderId = isInitiator.dataValues.order_id;
const result = await Orders.findOne({
where: {
id: orderId,
},
attributes: [
"id",
"cafe_id",
"group_coffee_run_id",
"user_id",
"estimated_arival_time",
],
});
const cafeData = await Cafes.findOne({
where: {
id: result.cafe_id,
},
});
const message = `${userData.name} has placed a new order. Order number is ${result.dataValues.group_coffee_run_id}`;
//send notification to cafe
await NotificationList.create({
sender_id: userData.id,
receiver_id: cafeData.id,
reference_id: isInitiator.id,
notification_type: 20,
message: message,
is_read: 0,
created_at: moment().unix(),
updated_at: moment().unix(),
});
const notification_response = notification.newOrderNotification({
recieverId: result.cafe_id,
orderId: result.dataValues.id,
senderId: result.user_id,
message: message,
estimated_arival_time: result.estimated_arival_time,
is_individual_order: 0,
});
console.log("notification", notification_response);
}
console.log(`Order status updated for coffee run: ${data.coffeeRunId}`);
});
} catch (e) {
console.log(e);
throw e;
}
};
import { v4 as uuidv4 } from "uuid"; // Ensure you have uuid installed
async function syncGroupOrdersToSquareGrouped(coffeeRunId) {
try {
const orders = await Orders.findAll({
where: { request_unique_id: coffeeRunId },
});
if (!orders.length) return;
const cafeId = orders[0].cafe_id;
const cafe = await Cafes.findOne({
where: { id: cafeId },
});
if (!cafe || !cafe.square_access_token || !cafe.square_location_id) {
console.error("Missing cafe Square config");
return;
}
const locationId = JSON.parse(cafe.square_location_id)[0];
const allLineItems = [];
// Get user for pickup info (initiator)
const initiator = await User.findOne({ where: { id: orders[0].user_id } });
//const pickupTime = new Date(Date.now() + 10 * 60 * 1000).toISOString();
let pickupTime;
const estimatedTimeRaw = orders?.[0]?.estimated_arival_time;
if (!estimatedTimeRaw || estimatedTimeRaw.toLowerCase() === "asap") {
// Use current time
pickupTime = new Date().toISOString();
//pickupTime = new Date(Date.now() + 05 * 60 * 1000).toISOString();
} else {
try {
const timeStr = estimatedTimeRaw.trim(); // e.g., "05:59"
const [hoursStr, minutesStr] = timeStr.split(":");
const hours = parseInt(hoursStr, 10);
const minutes = parseInt(minutesStr, 10);
if (
isNaN(hours) ||
isNaN(minutes) ||
hours < 0 || hours > 23 ||
minutes < 0 || minutes > 59
) {
throw new Error("Invalid estimated arrival time format: " + timeStr);
}
const now = new Date();
const composedDate = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
hours,
minutes,
0
);
pickupTime = composedDate.toISOString();
} catch (err) {
throw new Error("Failed to parse estimated arrival time: " + err.message);
}
}
for (const order of orders) {
const user = await User.findOne({ where: { id: order.user_id } });
const orderItems = JSON.parse(order.order_item_array || "[]");
orderItems.forEach((item) => {
const lineItem = {
name: item.item_name,
quantity: item.item_quantity.toString(),
base_price_money: {
amount: Math.round(Number(item.item_amount) * 100),
currency: "GBP",
},
note: `User: ${user?.name || "Guest"} | Order ID: ${order.id}`,
};
if (
item.addon_sizes &&
Array.isArray(item.addon_sizes) &&
item.addon_sizes.length > 0
) {
lineItem.modifiers = item.addon_sizes.map((addon) => ({
name: `${addon.addon_name} - ${addon.addon_size_name}`,
base_price_money: {
amount: 0,
currency: "GBP",
},
quantity: addon.quantity ? addon.quantity.toString() : "1",
}));
}
allLineItems.push(lineItem);
});
}
const orderPayload = {
order: {
location_id: locationId,
line_items: allLineItems,
fulfillments: [
{
type: "PICKUP",
state: "PROPOSED",
pickup_details: {
recipient: {
display_name: initiator?.name || "Coffee Run",
phone_number: initiator?.customer_phone || "",
email_address: initiator?.email || "",
},
pickup_at: pickupTime,
note: `Coffee run group order: ${coffeeRunId}`,
},
},
],
metadata: {
source: "Javago Group Order",
coffee_run_id: coffeeRunId,
},
},
idempotency_key: `grouped-${coffeeRunId}-${Date.now()}`,
};
const squareOrderRes = await fetch(
"https://connect.squareup.com/v2/orders",
{
method: "POST",
headers: {
Authorization: `Bearer ${cafe.square_access_token}`,
"Content-Type": "application/json",
"Square-Version": "2024-06-04",
},
body: JSON.stringify(orderPayload),
}
);
const orderResult = await squareOrderRes.json();
if (!squareOrderRes.ok || !orderResult.order?.id) {
console.error("❌ Square grouped order failed:", orderResult);
return;
}
const squareOrderId = orderResult.order.id;
// Now mark payment as external (cash or other)
const paymentPayload = {
source_id: "EXTERNAL",
order_id: squareOrderId,
amount_money: {
amount: orderResult.order.total_money.amount,
currency: orderResult.order.total_money.currency,
},
external_details: {
type: "OTHER",
source: "Javago Cafe App - Pay on Pickup",
source_id: `javago-${orders[0].id}`,
source_fee_money: {
amount: 0,
currency: orderResult.order.total_money.currency,
},
},
idempotency_key: `payment-${orders[0].id}-${Date.now()}`,
autocomplete: true,
note: "Pre-authorized payment - Customer will pay on pickup",
};
const paymentRes = await fetch("https://connect.squareup.com/v2/payments", {
method: "POST",
headers: {
Authorization: `Bearer ${cafe.square_access_token}`,
"Content-Type": "application/json",
"Square-Version": "2024-06-04",
},
body: JSON.stringify(paymentPayload),
});
const paymentResult = await paymentRes.json();
if (!paymentRes.ok || !paymentResult.payment?.id) {
console.error("❌ Square payment failed:", paymentResult);
return;
}
// ✅ Save the Square order ID to all matching local orders
await Orders.update(
{
square_order_id: squareOrderId,
square_order_state: orderResult.order.state,
square_created_at: orderResult.order.created_at,
},
{ where: { request_unique_id: coffeeRunId } }
);
console.log(
`✅ Grouped order created and saved for coffeeRunId: ${coffeeRunId}`
);
} catch (err) {
console.error("🚨 syncGroupOrdersToSquareGrouped error:", err);
}
}
async function oldsyncGroupOrdersToSquareGrouped(coffeeRunId) {
try {
const orders = await Orders.findAll({
where: { request_unique_id: coffeeRunId },
});
if (!orders.length) {
console.warn(`No orders found for coffeeRunId: ${coffeeRunId}`);
return;
}
const cafe_id = orders[0].cafe_id;
// Fetch cafe separately
const cafe = await Cafes.findOne({
where: { id: cafe_id },
});
if (!cafe || !cafe.square_access_token || !cafe.square_location_id) {
console.error("Missing cafe Square config");
return;
}
const locationId = JSON.parse(cafe.square_location_id)[0];
const allLineItems = [];
for (const order of orders) {
const user = await User.findOne({ where: { id: order.user_id } });
const orderItems = JSON.parse(order.order_item_array || "[]");
orderItems.forEach((item) => {
const lineItem = {
name: item.item_name,
quantity: item.item_quantity.toString(),
base_price_money: {
amount: Math.round(Number(item.item_amount) * 100),
currency: "GBP",
},
note: `User: ${user?.name || "Unknown"} | Order ID: ${order.id}`,
};
if (Array.isArray(item.addon_sizes) && item.addon_sizes.length > 0) {
lineItem.modifiers = item.addon_sizes.map((addon) => ({
name: `${addon.addon_name} - ${addon.addon_size_name}`,
base_price_money: {
amount: 0,
currency: "GBP",
},
quantity: addon.quantity ? addon.quantity.toString() : "1",
}));
}
allLineItems.push(lineItem);
});
}
// Use first user's info as the initiator
const initiator = await User.findOne({ where: { id: orders[0].user_id } });
const pickupTime = new Date(Date.now() + 10 * 60 * 1000).toISOString();
const orderPayload = {
order: {
location_id: locationId,
line_items: allLineItems,
fulfillments: [
{
type: "PICKUP",
state: "PROPOSED",
pickup_details: {
recipient: {
display_name: initiator?.name || "Coffee Run",
phone_number: initiator?.customer_phone || "",
email_address: initiator?.email || "",
},
pickup_at: pickupTime,
note: `Coffee run group order: ${coffeeRunId}`,
},
},
],
metadata: {
source: "Javago Group Order",
coffee_run_id: coffeeRunId,
},
},
idempotency_key: `grouped-${coffeeRunId}-${Date.now()}`,
};
// Create Square Order
const squareRes = await fetch("https://connect.squareup.com/v2/orders", {
method: "POST",
headers: {
Authorization: `Bearer ${cafe.square_access_token}`,
"Content-Type": "application/json",
"Square-Version": "2024-06-04",
},
body: JSON.stringify(orderPayload),
});
const squareResult = await squareRes.json();
if (squareRes.ok && squareResult.order?.id) {
const squareOrderId = squareResult.order.id;
// Save the same Square order ID to all Orders
await Orders.update(
{
square_order_id: squareOrderId,
square_order_state: squareResult.order.state,
square_created_at: squareResult.order.created_at,
},
{ where: { request_unique_id: coffeeRunId } }
);
// 💳 Create payment to make it visible in Square dashboard
const paymentPayload = {
source_id: "EXTERNAL",
order_id: squareOrderId,
amount_money: {
amount: squareResult.order.total_money.amount,
currency: squareResult.order.total_money.currency,
},
external_details: {
type: "OTHER",
source: "Javago Cafe App - Pay on Pickup",
source_id: `javago-${orders[0].id}`,
source_fee_money: {
amount: 0,
currency: squareResult.order.total_money.currency,
},
},
idempotency_key: `payment-${orders[0].id}-${Date.now()}`,
autocomplete: true,
note: "Pre-authorized payment - Customer will pay on pickup",
};
const paymentRes = await fetch(
"https://connect.squareup.com/v2/payments",
{
method: "POST",
headers: {
Authorization: `Bearer ${cafe.square_access_token}`,
"Content-Type": "application/json",
"Square-Version": "2024-06-04",
},
body: JSON.stringify(paymentPayload),
}
);
const paymentResult = await paymentRes.json();
if (paymentRes.ok && paymentResult.payment?.id) {
await Orders.update(
{
square_payment_id: paymentResult.payment.id,
square_payment_status: paymentResult.payment.status,
square_paid_at: new Date(),
},
{ where: { request_unique_id: coffeeRunId } }
);
console.log(
`✅ Grouped order & payment created for coffeeRunId: ${coffeeRunId}`
);
} else {
console.error("❌ Square payment creation failed:", paymentResult);
}
} else {
console.error("❌ Square order creation failed:", squareResult);
}
} catch (err) {
console.error("🚨 syncGroupOrdersToSquareGrouped error:", err);
}
}
export default schedule;