// ==========================================
// 🏏 SIKKA GAME - BACKEND ENGINE (1000+ Users)
// ==========================================
const ROUND_DURATION = 30000; // 30 Seconds Total
const BETTING_WINDOW = 23000; // 23 Seconds Open
function doPost(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var req = JSON.parse(e.postData.contents);
var action = req.action;
if (action === "register") return ContentService.createTextOutput(JSON.stringify(registerUser(ss, req))).setMimeType(ContentService.MimeType.JSON);
if (action === "login") return ContentService.createTextOutput(JSON.stringify(loginUser(ss, req))).setMimeType(ContentService.MimeType.JSON);
// SESSION AUTHENTICATION
var auth = verifySession(ss, req.phone, req.token);
if (!auth.success) return ContentService.createTextOutput(JSON.stringify(auth)).setMimeType(ContentService.MimeType.JSON);
var res;
if (action === "getUserData") res = getUserData(ss, req.phone);
else if (action === "deposit") res = depositFunds(ss, req);
else if (action === "withdraw") res = withdrawFunds(ss, req);
else if (action === "tossGetRound") res = tossGetRound(ss);
else if (action === "tossPlaceBet") res = tossPlaceBet(ss, req);
else if (action === "tossResolve") res = tossResolve(ss, req);
else res = { success: false, message: "Invalid API Action" };
return ContentService.createTextOutput(JSON.stringify(res)).setMimeType(ContentService.MimeType.JSON);
}
// 🔐 SESSION TOKEN AUTHENTICATION (Updated for 11 Columns)
function verifySession(ss, phone, token) {
var sheet = ss.getSheetByName("Users");
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
if (data[i][2].toString() === phone.toString()) {
// Index 9 = Token (Col J)
if (data[i][9] === token) {
sheet.getRange(i + 1, 7).setValue(new Date()); // Update Last Active (Col G)
return { success: true };
}
return { success: false, message: "Session Expired! Logged in from another device." };
}
}
return { success: false, message: "User not found." };
}
// 🚫 REGISTRATION & ANTI-SPAM BLOCKER
function registerUser(ss, data) {
var otpSheet = ss.getSheetByName("Common OTP");
var validOtp = otpSheet.getRange("A2").getValue().toString().trim();
if (data.otp.toString().trim() !== validOtp) return { success: false, message: "Invalid Admin OTP!" };
var phoneStr = data.phone.toString();
// Anti-Spam: Block repeated digits
if (/^(\d)\1{9}$/.test(phoneStr)) return { success: false, message: "Spam/Fake numbers are not allowed!" };
if (phoneStr.length !== 10) return { success: false, message: "Invalid 10-digit number." };
var sheet = ss.getSheetByName("Users");
var users = sheet.getDataRange().getValues();
for (var i = 1; i < users.length; i++) {
if (users[i][2].toString() === phoneStr) return { success: false, message: "Mobile number already registered!" };
}
var newId = "U" + Math.floor(Math.random() * 90000 + 10000);
// EXACT 11 COLUMNS: A(ID), B(Name), C(Phone), D(UPI), E(Play Bal), F(Win Bal), G(Last Active), H(Status), I(Pass), J(Token), K(Referred By)
sheet.appendRow([newId, data.name, phoneStr, "", 0.00, 0.00, new Date(), "Active", data.pass, "", data.refCode || ""]);
return { success: true, message: "Account Created!" };
}
// 🔑 LOGIN SYSTEM (Updated for 11 Columns)
function loginUser(ss, data) {
var sheet = ss.getSheetByName("Users");
var users = sheet.getDataRange().getValues();
for (var i = 1; i < users.length; i++) {
// Index 8 = Password (Col I)
if (users[i][2].toString() === data.phone.toString() && users[i][8].toString() === data.pass) {
// Index 7 = Status (Col H)
if (users[i][7] !== "Active") return { success: false, message: "Account is Inactive/Archived." };
var newToken = Utilities.getUuid();
sheet.getRange(i + 1, 10).setValue(newToken); // Token save in Col J
sheet.getRange(i + 1, 7).setValue(new Date()); // Last Active save in Col G
// Send Combined Balance for frontend display
var totalBal = (parseFloat(users[i][4]) || 0) + (parseFloat(users[i][5]) || 0);
return { success: true, token: newToken, balance: totalBal.toFixed(2) };
}
}
return { success: false, message: "Invalid Mobile or Password!" };
}
// 👤 GET USER DATA & SMART HISTORY LOADER (Updated for Two Wallets)
function getUserData(ss, phone) {
var uSheet = ss.getSheetByName("Users");
var uData = uSheet.getDataRange().getValues();
var totalBal = 0;
for (var i = 1; i < uData.length; i++) {
if (uData[i][2].toString() === phone.toString()) {
// Play Balance (Index 4) + Winning Balance (Index 5)
totalBal = (parseFloat(uData[i][4]) || 0) + (parseFloat(uData[i][5]) || 0);
break;
}
}
var tSheet = ss.getSheetByName("Transactions");
var tData = tSheet.getDataRange().getValues();
var history = [];
for (var j = tData.length - 1; j >= 1; j--) {
if (tData[j][2].toString() === phone.toString()) {
var amt = parseFloat(tData[j][4]);
history.push({ date: tData[j][1], type: tData[j][3], amount: amt, status: tData[j][5] });
if (history.length >= 10) break; // Limit to 10
}
}
return { success: true, balance: totalBal.toFixed(2), history: history };
}
// 💰 DEPOSIT FUNDS (₹50 - ₹1000 Limit) + Concurrency Lock
function depositFunds(ss, req) {
var lock = LockService.getScriptLock();
// Agar ek sath kai log deposit kar rahe hain, to system 5 second wait karayega
if (!lock.tryLock(5000)) return { success: false, message: "System busy, please try again in 5 seconds." };
try {
var amount = parseFloat(req.amount);
if (amount < 50 || amount > 1000) return { success: false, message: "Deposit limit is ₹50 to ₹1000." };
var tSheet = ss.getSheetByName("Transactions");
var txnId = "TXN" + Math.floor(Math.random() * 900000 + 100000);
// Naya row add karna
tSheet.appendRow([txnId, new Date(), req.phone, "Deposit", amount.toFixed(2), "Pending", req.utr]);
// Dropdown/Color fix apply karna
var lastRow = tSheet.getLastRow();
if (lastRow > 2) {
var sourceCell = tSheet.getRange(lastRow - 1, 6);
var targetCell = tSheet.getRange(lastRow, 6);
sourceCell.copyTo(targetCell);
targetCell.setValue("Pending");
}
return { success: true };
} finally {
// Process hone ke baad lock open kar dena
lock.releaseLock();
}
}
// 🏦 WITHDRAW FUNDS (Updated for Winning Balance Deduction Only)
function withdrawFunds(ss, req) {
var lock = LockService.getScriptLock();
if (!lock.tryLock(5000)) return { success: false, message: "Server busy, try again in 5 seconds." };
try {
var amount = parseFloat(req.amount);
if (amount < 100 || amount > 2000) return { success: false, message: "Withdrawal limit is ₹100 to ₹2000." };
var tSheet = ss.getSheetByName("Transactions");
var tData = tSheet.getDataRange().getValues();
var todayStr = new Date().toDateString();
var dailyCount = 0;
for (var j = tData.length - 1; j >= 1; j--) {
if (tData[j][2].toString() === req.phone.toString() && tData[j][3] === "Withdrawal") {
var txnDateStr = new Date(tData[j][1]).toDateString();
if (txnDateStr === todayStr) dailyCount++;
}
}
if (dailyCount >= 1) return { success: false, message: "Only 1 withdrawal allowed per day." };
var uSheet = ss.getSheetByName("Users");
var uData = uSheet.getDataRange().getValues();
for (var i = 1; i < uData.length; i++) {
if (uData[i][2].toString() === req.phone.toString()) {
// ONLY check Winning Balance (Index 5 / Col F) for Withdrawal
var winBal = parseFloat(uData[i][5]) || 0;
if (winBal < amount) return { success: false, message: "Insufficient Winning Balance!" };
// Auto-save UPI ID (Col 4 / D) & Deduct from Winning Balance (Col 6 / F)
uSheet.getRange(i + 1, 4).setValue(req.upiId);
uSheet.getRange(i + 1, 6).setValue((winBal - amount).toFixed(2));
var txnId = "TXN" + Math.floor(Math.random() * 900000 + 100000);
tSheet.appendRow([txnId, new Date(), req.phone, "Withdrawal", "-" + amount.toFixed(2), "Pending", req.upiId]);
var lastRow = tSheet.getLastRow();
if (lastRow > 2) {
var sourceCell = tSheet.getRange(lastRow - 1, 6);
var targetCell = tSheet.getRange(lastRow, 6);
sourceCell.copyTo(targetCell);
targetCell.setValue("Pending");
}
return { success: true };
}
}
} finally {
lock.releaseLock();
}
}
// 🪙 GET ROUND DATA
function tossGetRound(ss) {
var sheet = ss.getSheetByName("Toss_Rounds");
var data = sheet.getDataRange().getValues();
var now = new Date().getTime();
var activeRound = null;
var history = [];
for (var i = data.length - 1; i >= 1; i--) {
var status = data[i][2];
if ((status === "Completed" || status === "Server Issue") && history.length < 30) {
history.push({ roundId: data[i][0], result: data[i][3] });
}
if (status === "Active" || status === "Processing" || status === "Server Issue") {
var roundTimestamp = new Date(data[i][1]).getTime();
if (now - roundTimestamp < ROUND_DURATION) {
if(!activeRound) activeRound = { roundId: data[i][0], timestamp: roundTimestamp };
} else {
if (status === "Active") sheet.getRange(i + 1, 3).setValue("Processing");
}
}
}
if (!activeRound) {
var newRoundId = "RND-" + Math.floor(Math.random() * 1000000);
var createTime = new Date();
sheet.appendRow([newRoundId, createTime, "Active", "Pending"]);
activeRound = { roundId: newRoundId, timestamp: createTime.getTime() };
}
return { success: true, round: activeRound, history: history, serverTime: new Date().getTime() };
}
// 💰 PLACE BET (Updated for Two-Wallet Deduction Logic)
function tossPlaceBet(ss, req) {
var lock = LockService.getScriptLock();
if (!lock.tryLock(5000)) return { success: false, message: "System is processing, please wait." };
try {
var amount = parseFloat(req.amount);
if (amount < 5 || amount > 100) return { success: false, message: "Bet limit is ₹5 to ₹100." };
if (req.side !== "HEAD" && req.side !== "TAIL") return { success: false, message: "Invalid bet side." };
var rSheet = ss.getSheetByName("Toss_Rounds");
var rData = rSheet.getDataRange().getValues();
var rIndex = -1;
var roundTimestamp = 0;
for (var i = rData.length - 1; i >= 1; i--) {
if (rData[i][0] === req.roundId) {
rIndex = i + 1;
roundTimestamp = new Date(rData[i][1]).getTime();
break;
}
}
if (rIndex === -1) return { success: false, message: "Round not found." };
if (new Date().getTime() - roundTimestamp > BETTING_WINDOW) {
return { success: false, message: "Betting is closed for this round!" };
}
var bSheet = ss.getSheetByName("Toss_Bets");
var bData = bSheet.getDataRange().getValues();
var todayStr = new Date().toDateString();
var userBetToday = 0;
var currentRoundBettors = [];
for (var k = bData.length - 1; k >= 1; k--) {
var bDate = new Date(bData[k][1]);
if (bData[k][2].toString() === req.phone.toString() && bDate.toDateString() === todayStr) {
userBetToday++;
}
if (bData[k][1] === req.roundId) {
if (currentRoundBettors.indexOf(bData[k][2].toString()) === -1) currentRoundBettors.push(bData[k][2].toString());
}
}
if (currentRoundBettors.length === 0 || (currentRoundBettors.length === 1 && currentRoundBettors[0] === req.phone.toString())) {
if (amount > 20) return { success: false, message: "Single player bet limit is ₹20." };
if (userBetToday >= 5) return { success: false, message: "Single player limit of 5 bets/day reached." };
}
// Deduct Balance (Play Balance first, then Winning Balance)
var uSheet = ss.getSheetByName("Users");
var uData = uSheet.getDataRange().getValues();
var uIndex = -1;
for (var j = 1; j < uData.length; j++) {
if (uData[j][2].toString() === req.phone.toString()) {
uIndex = j + 1;
var playBal = parseFloat(uData[j][4]) || 0; // Col E
var winBal = parseFloat(uData[j][5]) || 0; // Col F
if ((playBal + winBal) < amount) return { success: false, message: "Insufficient Wallet Balance!" };
// Deduction Logic
if (playBal >= amount) {
uSheet.getRange(uIndex, 5).setValue((playBal - amount).toFixed(2));
} else {
var remaining = amount - playBal;
uSheet.getRange(uIndex, 5).setValue(0.00); // Play Bal becomes 0
uSheet.getRange(uIndex, 6).setValue((winBal - remaining).toFixed(2)); // Rest from Win Bal
}
break;
}
}
var betId = "TB-" + Math.floor(Math.random() * 1000000);
bSheet.appendRow([betId, req.roundId, req.phone, req.side, amount.toFixed(2), "Locked", 0]);
return { success: true };
} finally {
lock.releaseLock();
}
}
// 🏆 RESOLVE ROUND (Updated for Winning Balance Distribution)
function tossResolve(ss, req) {
var lock = LockService.getScriptLock();
if (!lock.tryLock(5000)) return { success: true, result: window.tossResultCache };
try {
var rSheet = ss.getSheetByName("Toss_Rounds");
var rData = rSheet.getDataRange().getValues();
var rIndex = -1;
for (var i = rData.length - 1; i >= 1; i--) {
if (rData[i][0] === req.roundId) {
if (rData[i][2] === "Completed" || rData[i][2] === "Server Issue") return { success: true, result: rData[i][3] };
rIndex = i + 1;
break;
}
}
if (rIndex === -1) return { success: false };
var bSheet = ss.getSheetByName("Toss_Bets");
var bData = bSheet.getDataRange().getValues();
var headSum = 0; var tailSum = 0;
var roundBets = [];
var uniquePlayers = [];
for (var j = 1; j < bData.length; j++) {
if (bData[j][1] === req.roundId) {
roundBets.push({ row: j + 1, phone: bData[j][2], side: bData[j][3], amount: parseFloat(bData[j][4]) });
if (bData[j][3] === "HEAD") headSum += parseFloat(bData[j][4]);
else if (bData[j][3] === "TAIL") tailSum += parseFloat(bData[j][4]);
if (uniquePlayers.indexOf(bData[j][2].toString()) === -1) uniquePlayers.push(bData[j][2].toString());
}
}
var finalResult = "HEAD";
var totalPool = headSum + tailSum;
if (totalPool === 0) finalResult = Math.random() < 0.5 ? "HEAD" : "TAIL";
else if (uniquePlayers.length === 1) finalResult = Math.random() < 0.5 ? "HEAD" : "TAIL";
else if (headSum === tailSum) finalResult = "SERVER_ISSUE";
else finalResult = (headSum < tailSum) ? "HEAD" : "TAIL";
rSheet.getRange(rIndex, 3).setValue(finalResult === "SERVER_ISSUE" ? "Server Issue" : "Completed");
rSheet.getRange(rIndex, 4).setValue(finalResult);
var uSheet = ss.getSheetByName("Users");
var uData = uSheet.getDataRange().getValues();
var distributed = 0;
for (var k = 0; k < roundBets.length; k++) {
var bet = roundBets[k];
var wonAmt = 0;
var newStatus = "Lost";
if (finalResult === "SERVER_ISSUE") {
wonAmt = bet.amount; // 100% Refund
newStatus = "Refunded";
} else if (bet.side === finalResult) {
wonAmt = bet.amount * 1.9; // 1.9x Winning
newStatus = "Won";
distributed += wonAmt;
}
if (wonAmt > 0) {
bSheet.getRange(bet.row, 6).setValue(newStatus);
bSheet.getRange(bet.row, 7).setValue(wonAmt.toFixed(2));
for (var m = 1; m < uData.length; m++) {
if (uData[m][2].toString() === bet.phone.toString()) {
if (finalResult === "SERVER_ISSUE") {
// Refunds go back to Play Balance (Col E / 5)
var playBal = parseFloat(uSheet.getRange(m + 1, 5).getValue()) || 0;
uSheet.getRange(m + 1, 5).setValue((playBal + wonAmt).toFixed(2));
} else {
// Winnings go direct to Winning Balance (Col F / 6)
var winBal = parseFloat(uSheet.getRange(m + 1, 6).getValue()) || 0;
uSheet.getRange(m + 1, 6).setValue((winBal + wonAmt).toFixed(2));
}
break;
}
}
} else {
bSheet.getRange(bet.row, 6).setValue("Lost");
}
}
if (finalResult !== "SERVER_ISSUE") {
var adminProfit = totalPool - distributed;
ss.getSheetByName("Admin_Profit").appendRow([req.roundId, totalPool.toFixed(2), adminProfit.toFixed(2), distributed.toFixed(2), new Date()]);
}
return { success: true, result: finalResult };
} finally {
lock.releaseLock();
}
}
// ==========================================
// 🔄 AUTO WALLET UPDATE ON MANUAL APPROVAL (100% FIXED)
// ==========================================
function onEdit(e) {
if (!e || !e.range) return;
var sheet = e.range.getSheet();
if (sheet.getName() !== "Transactions") return;
var col = e.range.getColumn();
var row = e.range.getRow();
if (col === 6 && row > 1) {
var status = sheet.getRange(row, 6).getDisplayValue().trim();
var oldStatus = e.oldValue ? e.oldValue.toString().trim() : "";
if (status === "Approved" && oldStatus !== "Approved") {
var type = sheet.getRange(row, 4).getDisplayValue().trim();
var phone = sheet.getRange(row, 3).getDisplayValue().trim();
var amount = parseFloat(sheet.getRange(row, 5).getValue());
if (type === "Deposit" && !isNaN(amount) && amount > 0) {
var usersSheet = e.source.getSheetByName("Users");
var usersData = usersSheet.getDataRange().getDisplayValues();
for (var i = 1; i < usersData.length; i++) {
var userPhone = usersData[i][2].toString().trim();
if (userPhone === phone) {
// Deposit goes to Play Balance (Col E / 5)
var currentBal = parseFloat(usersSheet.getRange(i + 1, 5).getValue()) || 0;
var newBal = currentBal + amount;
usersSheet.getRange(i + 1, 5).setValue(newBal.toFixed(2));
// 🔥 REFERRAL BONUS LOGIC
if (amount >= 100) {
var referredByPhone = usersData[i][10] ? usersData[i][10].toString().trim() : "";
if (referredByPhone !== "" && referredByPhone !== "BONUS_PAID") {
for (var r = 1; r < usersData.length; r++) {
if (usersData[r][2].toString().trim() === referredByPhone) {
var refBal = parseFloat(usersSheet.getRange(r + 1, 5).getValue()) || 0;
usersSheet.getRange(r + 1, 5).setValue((refBal + 50).toFixed(2));
var bonusTxnId = "TXN" + Math.floor(Math.random() * 900000 + 100000);
e.source.getSheetByName("Transactions").appendRow([
bonusTxnId, new Date(), referredByPhone, "Referral Bonus", "50.00", "Approved", "From: " + phone
]);
usersSheet.getRange(i + 1, 11).setValue("BONUS_PAID");
break;
}
}
}
}
break;
}
}
}
}
}
}
// ==========================================
// 🕒 CRON JOBS (Auto Database Cleanup & Lifecycle)
// ==========================================
function cleanupTossHistory() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var rSheet = ss.getSheetByName("Toss_Rounds");
var rLast = rSheet.getLastRow();
if (rLast > 100) rSheet.deleteRows(2, rLast - 100);
var bSheet = ss.getSheetByName("Toss_Bets");
var bLast = bSheet.getLastRow();
if (bLast > 300) bSheet.deleteRows(2, bLast - 300);
}
function userLifecycleManager() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Users");
var inSheet = ss.getSheetByName("Inactive_Users");
var arSheet = ss.getSheetByName("Archive_Users");
var data = sheet.getDataRange().getValues();
var now = new Date().getTime();
for (var i = data.length - 1; i >= 1; i--) {
// Index 6 = Last Active (Col G)
var lastActive = new Date(data[i][6]).getTime();
var daysInactive = (now - lastActive) / (1000 * 3600 * 24);
if (daysInactive >= 50) {
sheet.deleteRow(i + 1);
} else if (daysInactive >= 30) {
arSheet.appendRow(data[i]);
sheet.deleteRow(i + 1);
} else if (daysInactive >= 15) {
inSheet.appendRow(data[i]);
sheet.deleteRow(i + 1);
}
}
}