Dine in backend script GAS Code OCR LOGIC only Plain Text - NO Token, success✅2
// ==========================================
// PT SEVA: DINE-IN SAAS BACKEND (GAS) v13.7
// Model: Smart Subscription Dates + Monthly Sale Shift + Gemini AI Scanner
// Status: Column Mapping Fixed (V, W, X, Y) & Bug-Free
// =========================================
const SPREADSHEET_ID = "18hjPH8aBJmT8xzCU1k56QIVvH_u4R79xk31wBVA-fY4";
const S_USERS = "Users";
const S_REST = "Restaurants";
const S_MENU = "Menu";
const S_ORDERS = "Orders";
const S_TXN = "Transactions";
const S_ARCHIVE = "Archive";
const S_ADMIN = "Admin_Settings";
const S_CUSTOMERS = "Customers";
function jsonResponse(data) {
return ContentService.createTextOutput(JSON.stringify(data)).setMimeType(ContentService.MimeType.JSON);
}
function doGet(e) {
if (!e || !e.parameter || !e.parameter.action) {
return HtmlService.createHtmlOutputFromFile('Index')
.setTitle('PT Seva Foodie & Partner App')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
.addMetaTag('viewport', 'width=device-width, initial-scale=1');
}
const action = e.parameter.action;
if (action === "getMenu") return jsonResponse(fetchLiveMenu(e.parameter.restId || "MFF_01"));
if (action === "checkStatus") return jsonResponse(checkOrderStatus(e.parameter.orderId));
if (action === "restLogin") return jsonResponse(restLogin(e.parameter.id, e.parameter.pass));
if (action === "getRestStats") return jsonResponse(getRestStats(e.parameter.restId));
if (action === "getOwnerMenu") return jsonResponse(getOwnerMenu(e.parameter.restId));
if (action === "getCustomers") return jsonResponse(getMyCustomers(e.parameter.restId));
if (action === "toggleStore") return jsonResponse(toggleStore(e.parameter.restId));
if (action === "toggleItem") return jsonResponse(toggleItem(e.parameter.itemId, e.parameter.status));
if (action === "updatePrice") return jsonResponse(updateItemPrice(e.parameter.itemId, e.parameter.price));
}
function doPost(e) {
try {
const req = JSON.parse(e.postData.contents);
if (req.action === "placeOrder") return jsonResponse(placeOrder(req.orderPayload));
if (req.action === "registerRest") return jsonResponse(registerRestaurant(req.payload));
if (req.action === "addMenuItem") return jsonResponse(addMenuItem(req.payload));
if (req.action === "submitUTR") return jsonResponse(submitMonthlyUTR(req.payload));
if (req.action === "updateStatus") return jsonResponse(updateOrderStatus(req.payload.orderId, req.payload.newStatus, req.payload.etaTime));
if (req.action === "updateOrderItems") return jsonResponse(updateOrderItems(req.payload.orderId, req.payload.itemsStr));
if (req.action === "updateProfile") return jsonResponse(updateProfile(req.payload));
// 🔥 NEW: GEMINI OCR SCANNER ROUTE 🔥
if (req.action === "scanBill") return jsonResponse(processOCRWithGemini(req.payload.base64Image));
} catch (error) {
return jsonResponse({ status: "error", message: "Invalid Request Format" });
}
}
function getSheet(sheetName) {
return SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(sheetName);
}
function fetchLiveMenu(restId) {
try {
const restSheet = getSheet(S_REST).getDataRange().getValues();
let isStoreOpen = false;
let rName = "", rUpi = "", rQr = "";
for (let i = 1; i < restSheet.length; i++) {
if (restSheet[i][1] === restId) {
if (restSheet[i][11] === "OPEN") isStoreOpen = true;
rName = restSheet[i][3];
rUpi = restSheet[i][19];
rQr = restSheet[i][20];
break;
}
}
if (!isStoreOpen) return { status: "error", message: "Sorry, we are currently closed. Please try again later." };
const menuSheet = getSheet(S_MENU).getDataRange().getValues();
let menuData = [];
for (let i = 1; i < menuSheet.length; i++) {
if (menuSheet[i][0] === restId && menuSheet[i][9] === "Active") {
menuData.push({
id: menuSheet[i][1], name: menuSheet[i][2], desc: menuSheet[i][3],
price: menuSheet[i][4], type: menuSheet[i][5], category: menuSheet[i][6],
img: menuSheet[i][7], unit: menuSheet[i][8]
});
}
}
return { status: "success", data: menuData, restName: rName, restUpi: rUpi, restQr: rQr };
} catch (e) { return { status: "error", message: e.message }; }
}
function placeOrder(orderPayload) {
try {
const sheet = getSheet(S_ORDERS);
const orderId = "ORD" + Math.floor(10000 + Math.random() * 90000);
const timeNow = new Date();
let tableNumOnly = 0;
let tMatch = String(orderPayload.tableNumber || "").replace(/\[.*?\]/g, '').match(/T(\d+)/i);
if (tMatch) tableNumOnly = parseInt(tMatch[1], 10);
const decimal = tableNumOnly > 0 ? (tableNumOnly / 100) : 0;
const subTotal = parseFloat(orderPayload.subTotal) || 0;
const grandTotal = orderPayload.grandTotal ? parseFloat(orderPayload.grandTotal) : (subTotal + decimal);
let finalInstr = orderPayload.instructions || "";
if (orderPayload.isParcel === true) finalInstr = "[📦 PARCEL] " + finalInstr;
const orderType = orderPayload.orderType || "ONLINE";
const refId = orderPayload.refId || "";
const tokenPhotoUrl = orderPayload.tokenPhotoUrl || "";
const isPriority = orderPayload.isPriority || "FALSE";
const custType = orderPayload.customerType || "New";
sheet.appendRow([
timeNow, orderId, orderPayload.userPhone, orderPayload.restMobile,
orderPayload.restId, orderPayload.restName, orderPayload.items,
subTotal.toFixed(2), grandTotal.toFixed(2), decimal.toFixed(2),
orderPayload.tableNumber, orderPayload.payMode, "PENDING",
timeNow.toLocaleTimeString(), finalInstr, orderPayload.isParcel,
custType, "", "", "",
orderType, refId, tokenPhotoUrl, isPriority
]);
try {
const custSheet = getSheet(S_CUSTOMERS);
const custData = custSheet.getDataRange().getValues();
let isExistingCustomer = false;
for (let i = 1; i < custData.length; i++) {
if (custData[i][1] === orderPayload.restId && custData[i][2] == orderPayload.userPhone) {
custSheet.getRange(i + 1, 4).setValue(Number(custData[i][3]) + 1);
custSheet.getRange(i + 1, 5).setValue(timeNow);
isExistingCustomer = true; break;
}
}
if (!isExistingCustomer) custSheet.appendRow([ timeNow, orderPayload.restId, orderPayload.userPhone, 1, timeNow ]);
} catch (crmError) { }
return { status: "success", orderId: orderId, grandTotal: grandTotal.toFixed(2) };
} catch (e) { return { status: "error", message: e.message }; }
}
function checkOrderStatus(orderId) {
try {
const sheet = getSheet(S_ORDERS);
const data = sheet.getDataRange().getValues();
const now = new Date();
for (let i = data.length - 1; i >= 1; i--) {
if (data[i][1] === orderId) {
let stat = data[i][12];
let eta = data[i][19] || "";
let itemsStr = data[i][6] || "";
let payMode = data[i][11] || "";
// Prevent Auto-Cancel for Offline SCANNED & VIP Verify Orders
if (stat === "PENDING" && payMode !== "SCANNED" && payMode !== "UPI_VIP_VERIFY") {
let orderTime = new Date(data[i][0]);
if (Math.floor((now - orderTime) / 60000) >= 10) {
sheet.getRange(i + 1, 13).setValue("CANCELLED - AUTO");
stat = "CANCELLED - AUTO";
}
}
return { status: "success", orderStatus: stat, eta: eta, items: itemsStr };
}
}
return { status: "error", message: "Order not found" };
} catch (e) { return { status: "error", message: e.message }; }
}
function restLogin(phone, pass) {
try {
const sheet = getSheet(S_REST).getDataRange().getValues();
for (let i = 1; i < sheet.length; i++) {
if (sheet[i][6] == phone && sheet[i][7] == pass) {
if (sheet[i][18] !== "ACTIVE") return { status: "FAIL", msg: "Account pending Admin approval." };
return { status: "SUCCESS", restId: sheet[i][1], name: sheet[i][3] };
}
}
return { status: "FAIL", msg: "Invalid Mobile or Password." };
} catch (e) { return { status: "FAIL", msg: e.message }; }
}
function getRestStats(restId) {
try {
const restSheet = getSheet(S_REST).getDataRange().getValues();
let currentStatus = "CLOSED", profileData = {};
let aggregatedMonthlySale = 0, subFee = 0, subDue = 0;
for (let i = 1; i < restSheet.length; i++) {
if (restSheet[i][1] === restId) {
currentStatus = restSheet[i][11];
// Column Index Alignment
// V (21) = Current_Month_Sale
// W (22) = Subscription_Fee
// Y (24) = Subscription_Due (Assumed next available column after Last_Month_Sale)
aggregatedMonthlySale = parseFloat(restSheet[i][21]) || 0;
subFee = parseFloat(restSheet[i][22]) || 0;
subDue = parseFloat(restSheet[i][24]) || 0;
profileData = { mobile: restSheet[i][6], email: restSheet[i][13], fssai: restSheet[i][5], city: restSheet[i][8], address: restSheet[i][9], serviceType: restSheet[i][4], upiId: restSheet[i][19], qrImage: restSheet[i][20], image: restSheet[i][10] };
break;
}
}
let todayStr = new Date().toDateString();
let todayOrders = [], totalSaleLive = 0, totalSaleToday = 0;
const now = new Date();
const orderData = getSheet(S_ORDERS).getDataRange().getValues();
for (let i = 1; i < orderData.length; i++) {
if (orderData[i][4] === restId) {
let orderDateObj = new Date(orderData[i][0]);
let orderDate = orderDateObj.toDateString();
let stat = String(orderData[i][12]);
let grandTotal = parseFloat(orderData[i][8]) || 0;
let payMode = orderData[i][11] || "";
let oType = orderData[i][20] || "ONLINE";
let refId = orderData[i][21] || "";
let tokenPic = orderData[i][22] || "";
if (stat === "PENDING" && Math.floor((now - orderDateObj) / 60000) >= 10) {
if (payMode !== "SCANNED" && payMode !== "UPI_VIP_VERIFY") {
getSheet(S_ORDERS).getRange(i + 1, 13).setValue("CANCELLED - AUTO");
stat = "CANCELLED - AUTO";
}
}
if (!stat.includes("REJECTED") && !stat.includes("CANCELLED")) { totalSaleLive += grandTotal; }
if (orderDate === todayStr) {
if (!stat.includes("REJECTED") && !stat.includes("CANCELLED")) { totalSaleToday += grandTotal; }
todayOrders.push({
oid: orderData[i][1], tableNo: orderData[i][10], item: orderData[i][6],
amt: grandTotal.toFixed(2), grandTotal: grandTotal.toFixed(2),
status: stat, time: orderData[i][13], instr: orderData[i][14] || "",
isParcel: orderData[i][15], payMode: payMode, eta: orderData[i][19] || "",
orderType: oType, refId: refId, tokenPic: tokenPic
});
}
}
}
return {
status: "SUCCESS", currentStatus: currentStatus,
sale: totalSaleToday.toFixed(2),
monthlySale: (aggregatedMonthlySale + totalSaleLive).toFixed(2),
subscriptionFee: subFee.toFixed(2),
subscriptionDue: subDue.toFixed(2),
orders: todayOrders.reverse(), ...profileData
};
} catch (e) { return { status: "FAIL", msg: e.message }; }
}
function toggleStore(restId) {
try {
const sheet = getSheet(S_REST);
const data = sheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][1] === restId) {
let subDue = parseFloat(data[i][24]) || 0; // Check Due from Column Y (Index 24)
let currentStatus = data[i][11];
if (currentStatus === "CLOSED" && subDue > 0) return { status: "FAIL", msg: "⛔ Service Blocked! Please pay your Subscription Due." };
let newStatus = (currentStatus === "OPEN") ? "CLOSED" : "OPEN";
sheet.getRange(i + 1, 12).setValue(newStatus);
return { status: "SUCCESS", currentStatus: newStatus };
}
}
return { status: "FAIL", msg: "Not found" };
} catch (e) { return { status: "FAIL", msg: e.message }; }
}
function registerRestaurant(data) {
try {
const sheet = getSheet(S_REST);
const existing = sheet.getDataRange().getValues();
for (let i = 1; i < existing.length; i++) {
if (existing[i][6] == data.mobile) return { status: "error", message: "Mobile number already registered!" };
}
const restId = "RT" + Math.floor(1000 + Math.random() * 9000);
sheet.appendRow([
new Date(), restId, data.ownerName, data.shopName, data.serviceType,
data.fssai, data.mobile, data.password, data.city, data.address,
data.image, "CLOSED", "0", data.email, "No",
"", "", "", "PENDING", data.upiId, data.qrImage,
0, 999, 0, 0 // Initializing V, W, X, Y columns
]);
return { status: "success", message: "Registration successful. Waiting for Admin Approval.", restId: restId };
} catch (e) { return { status: "error", message: e.message }; }
}
function updateProfile(data) {
try {
const sheet = getSheet(S_REST);
const rows = sheet.getDataRange().getValues();
for (let i = 1; i < rows.length; i++) {
if (rows[i][1] === data.restId) {
sheet.getRange(i + 1, 5).setValue(data.serviceType); sheet.getRange(i + 1, 6).setValue(data.fssai); sheet.getRange(i + 1, 7).setValue(data.mobile);
if (data.password) sheet.getRange(i + 1, 8).setValue(data.password);
sheet.getRange(i + 1, 9).setValue(data.city); sheet.getRange(i + 1, 10).setValue(data.address);
if (data.image) sheet.getRange(i + 1, 11).setValue(data.image);
sheet.getRange(i + 1, 14).setValue(data.email); sheet.getRange(i + 1, 20).setValue(data.upiId);
if (data.qrImage) sheet.getRange(i + 1, 21).setValue(data.qrImage);
return { status: "success", message: "Profile Updated Successfully!" };
}
}
return { status: "error", message: "Account not found" };
} catch (e) { return { status: "error", message: e.message }; }
}
function getOwnerMenu(restId) {
try {
const menuSheet = getSheet(S_MENU).getDataRange().getValues();
let menuData = [];
for (let i = 1; i < menuSheet.length; i++) {
if (menuSheet[i][0] === restId) {
menuData.push({
id: menuSheet[i][1], nameEn: menuSheet[i][2], price: menuSheet[i][4],
type: menuSheet[i][5], catEn: menuSheet[i][6], img: menuSheet[i][7],
unit: menuSheet[i][8], status: menuSheet[i][9]
});
}
}
return { status: "SUCCESS", items: menuData.reverse() };
} catch (e) { return { status: "FAIL", msg: e.message }; }
}
function addMenuItem(data) {
try {
const sheet = getSheet(S_MENU);
const itemId = "ITM" + Math.floor(1000 + Math.random() * 9000);
sheet.appendRow([ data.restId, itemId, data.itemName, data.description || "", data.price, data.foodType, data.category, data.imageUrl || "", data.unit || "", "Active" ]);
return { status: "success", message: "Item added successfully", itemId: itemId };
} catch (e) { return { status: "error", message: e.message }; }
}
function toggleItem(itemId, newStatus) {
try {
const sheet = getSheet(S_MENU);
const data = sheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][1] === itemId) { sheet.getRange(i + 1, 10).setValue(newStatus); return { status: "SUCCESS" }; }
}
return { status: "FAIL", msg: "Item not found" };
} catch (e) { return { status: "FAIL", msg: e.message }; }
}
function updateItemPrice(itemId, newPrice) {
try {
const sheet = getSheet(S_MENU);
const data = sheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][1] === itemId) {
sheet.getRange(i + 1, 5).setValue(Number(newPrice));
return { status: "SUCCESS", message: "Price updated" };
}
}
return { status: "FAIL", msg: "Item not found" };
} catch (e) { return { status: "FAIL", msg: e.message }; }
}
function updateOrderStatus(orderId, newStatus, etaTime) {
try {
const sheet = getSheet(S_ORDERS);
const data = sheet.getDataRange().getValues();
const timeNow = new Date().toLocaleTimeString();
for (let i = 1; i < data.length; i++) {
if (data[i][1] == orderId) {
sheet.getRange(i + 1, 13).setValue(newStatus);
if (newStatus === "COOKING") {
sheet.getRange(i + 1, 18).setValue(timeNow);
if (etaTime) sheet.getRange(i + 1, 20).setValue(etaTime);
}
else if (newStatus === "COMPLETED" || newStatus === "DELIVERED" || newStatus === "READY") {
sheet.getRange(i + 1, 19).setValue(timeNow);
}
return { status: "success", message: "Order marked as " + newStatus };
}
}
return { status: "error", message: "Order not found" };
} catch (e) { return { status: "error", message: e.message }; }
}
function updateOrderItems(orderId, itemsStr) {
try {
const sheet = getSheet(S_ORDERS);
const data = sheet.getDataRange().getValues();
for (let i = data.length - 1; i >= 1; i--) {
if (data[i][1] === orderId) {
sheet.getRange(i + 1, 7).setValue(itemsStr);
return { status: "success", message: "Items updated successfully" };
}
}
return { status: "error", message: "Order not found" };
} catch (e) { return { status: "error", message: e.message }; }
}
function submitMonthlyUTR(data) {
try {
const sheet = getSheet(S_TXN);
const txnId = "TXN" + Math.floor(100000 + Math.random() * 900000);
sheet.appendRow([ new Date(), txnId, data.restId, data.restName, data.billingMonth, data.amountPaid, data.utrNumber, "PENDING", "" ]);
return { status: "success", message: "UTR Submitted. Waiting for Admin Verification." };
} catch (e) { return { status: "error", message: e.message }; }
}
function getMyCustomers(restId) {
try {
const custSheet = getSheet(S_CUSTOMERS).getDataRange().getValues();
let customerList = [];
for (let i = 1; i < custSheet.length; i++) {
if (custSheet[i][1] === restId) { customerList.push({ phone: custSheet[i][2], totalOrders: custSheet[i][3], lastOrderDate: new Date(custSheet[i][4]).toDateString() }); }
}
return { status: "SUCCESS", total: customerList.length, customers: customerList.reverse() };
} catch (e) { return { status: "FAIL", msg: e.message }; }
}
// 🔥 FEATURE: PERFECT COLUMN MAPPING & MONTHLY ROLLOVER
function midnightArchiveCron() {
const ordersSheet = getSheet(S_ORDERS);
const archiveSheet = getSheet(S_ARCHIVE);
const restSheet = getSheet(S_REST);
const ordersData = ordersSheet.getDataRange().getValues();
const restData = restSheet.getDataRange().getValues();
let rowsToDelete = [];
const today = new Date();
let saleToAdd = {};
// 1. Archive Completed/Cancelled Orders & Calculate Today's Sale
for (let i = ordersData.length - 1; i >= 1; i--) {
const status = String(ordersData[i][12]);
if (status === "COMPLETED" || status === "DELIVERED" || status.includes("REJECTED") || status.includes("CANCELLED")) {
if (!status.includes("REJECTED") && !status.includes("CANCELLED")) {
let restId = ordersData[i][4];
let grandTotal = parseFloat(ordersData[i][8]) || 0;
saleToAdd[restId] = (saleToAdd[restId] || 0) + grandTotal;
}
let rowData = ordersData[i];
rowData.push(today);
archiveSheet.appendRow(rowData);
rowsToDelete.push(i + 1);
}
}
rowsToDelete.sort((a,b) => b-a).forEach(rowIdx => ordersSheet.deleteRow(rowIdx));
// 2. Monthly Sale Rollover & Smart Subscription Check
const isFirstDayOfMonth = (today.getDate() === 1);
for (let i = 1; i < restData.length; i++) {
let restId = restData[i][1];
// Column V (21) = Current_Month_Sale
// Column W (22) = Subscription_Fee
// Column X (23) = Last_Month_Sale
// Column Y (24) = Subscription_Due (New safe column for Due)
let currentSale = parseFloat(restData[i][21]) || 0;
let subFee = parseFloat(restData[i][22]) || 0;
let currentDue = parseFloat(restData[i][24]) || 0;
// Column R (17) = Subscription_End_Date
let endDateVal = restData[i][17];
// 🗓️ Monthly Sale Rollover (1st of Month)
if (isFirstDayOfMonth) {
restSheet.getRange(i + 1, 24).setValue(currentSale); // Put Current Sale into Last_Month_Sale (Col X)
restSheet.getRange(i + 1, 22).setValue(saleToAdd[restId] || 0); // Reset Current Sale to today's sale (Col V)
} else {
if (saleToAdd[restId] > 0) {
restSheet.getRange(i + 1, 22).setValue(currentSale + saleToAdd[restId]); // Add to Current Sale
}
}
// 🔐 Smart Subscription Checking
if (endDateVal) {
let endDate = new Date(endDateVal);
if (!isNaN(endDate.getTime()) && today.getTime() > endDate.getTime()) {
if (currentDue === 0 && subFee > 0) {
restSheet.getRange(i + 1, 25).setValue(subFee); // Add Due to Column Y (Index 24)
restSheet.getRange(i + 1, 12).setValue("CLOSED"); // Lock Store
}
}
}
}
// 3. Clean up Archive older than 7 Days
const archData = archiveSheet.getDataRange().getValues();
for (let i = archData.length - 1; i >= 1; i--) {
let archDate = new Date(archData[i][24]);
let diffDays = Math.floor((today - archDate) / (1000 * 60 * 60 * 24));
if (diffDays >= 7) {
archiveSheet.deleteRow(i + 1);
}
}
}
// =========================================
// 🔥 GEMINI AI OCR ENGINE (SMART JSON MODE)
// ==========================================
function processOCRWithGemini(base64Image) {
try {
// 🔥 APNI GEMINI API KEY YAHAN DAALEIN 🔥
var apiKey = "AIzaSyBbJ-Pv1df886JSvHA9zkJTe-G-iBHJGXw";
var url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash-lite:generateContent?key=" + apiKey;
// 🔥 MAGIC PROMPT (UPDATED): AI ko Token No. aur Cart Items dono ek JSON Object mein dene ka order diya
var payload = {
"contents": [{
"parts": [
{"text": "Analyze this image. If it is a restaurant receipt or KOT, extract the Token/Bill/Order number and ONLY the ordered food/drink items with their quantities. Return the result STRICTLY as a JSON object. Example Format: {\"tokenNo\": \"25\", \"items\": [{\"name\": \"Dosa Spl\", \"qty\": 1}, {\"name\": \"Water Bottle 1lt\", \"qty\": 2}]}. If no token number is found, leave it empty: \"\". If the image is NOT a receipt or has no food items, return empty items: {\"tokenNo\": \"\", \"items\": []}. Do NOT include prices, totals, GST, or any other text. Do NOT wrap it in ```json markdown, just output the pure JSON object."},
{
"inline_data": {
"mime_type": "image/jpeg",
"data": base64Image
}
}
]
}]
};
var options = {
"method": "post",
"contentType": "application/json",
"payload": JSON.stringify(payload),
"muteHttpExceptions": true
};
var response = UrlFetchApp.fetch(url, options);
var json = JSON.parse(response.getContentText());
if (json.error) {
return { status: "error", message: json.error.message };
}
var aiResponse = json.candidates[0].content.parts[0].text;
// Clean markdown tags if AI sends them by mistake
aiResponse = aiResponse.replace(/```json/gi, '').replace(/```/g, '').trim();
return { status: "success", text: aiResponse };
} catch(e) {
return { status: "error", message: "Gemini API Error: " + e.toString() };
}
}