/** * Might Learn Premium Sheet API v2.1 * Bound to the "MightLearn Payments" spreadsheet. * Script property: ML_BRIDGE_SECRET (24+ characters). * Deployment: Web app, execute as Me; access Anyone for WP server calls. * Sheet: Membership Data (A Email, B Phone, C Amount, D Start Date, * E Expiry Date, F Status, G UTR). * Do not publish or log ML_BRIDGE_SECRET. */ const ML_MEMBERSHIP_TAB = 'Membership Data'; const ML_TIMEZONE = 'Asia/Kolkata'; function doGet() { return mlJson_({ success:true, service:'MightLearn Membership API', version:'2.1' }); } function doPost(e) { try { const data = JSON.parse((e && e.postData && e.postData.contents) || '{}'); const configured = PropertiesService.getScriptProperties().getProperty('ML_BRIDGE_SECRET') || ''; if (configured.length < 24 || !data.secret || data.secret !== configured) { return mlJson_({success:false, message:'Not authorized'}); } if (data.action !== 'checkMembership') { return mlJson_({success:false, message:'Unsupported action'}); } const email = String(data.email || '').trim().toLowerCase(); if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { return mlJson_({success:false, message:'Invalid email'}); } return mlJson_(mlLookupMembership_(email)); } catch (err) { console.error('Membership lookup failed:', String(err)); return mlJson_({success:false, message:'Membership lookup error'}); } } function mlLookupMembership_(email) { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(ML_MEMBERSHIP_TAB); if (!sheet) return {success:false, message:'Membership Data tab not found'}; const last = sheet.getLastRow(); const now = Date.now(); let winner = null; if (last > 1) { const rows = sheet.getRange(2,1,last-1,7).getValues(); for (let i=0; i now || end.getTime() <= now) continue; if (!winner || end.getTime() > winner.end.getTime()) { winner = {end:end, plan:amount === 49 ? 'yearly' : 'monthly'}; } } } return winner ? { success:true, active:true, plan:winner.plan, expiry:Utilities.formatDate(winner.end,ML_TIMEZONE,"yyyy-MM-dd'T'HH:mm:ssXXX") } : {success:true, active:false, plan:'free', expiry:''}; } /** * Convert date cells as India-local calendar dates. Expiry date-only cells are * valid until the END of that day, instead of expiring at 00:00 at its start. * Handles genuine Sheets Date values, dd/MM/yyyy, yyyy-MM-dd and timestamps. */ function mlAsDate_(value, endOfDay) { if (value == null || value === '') return null; const isDate = value instanceof Date && !isNaN(value.getTime()); if (isDate) { const d = new Date(value.getTime()); // Date-only cells commonly have midnight in the spreadsheet's time zone. // Use the displayed date as the authoritative calendar date. const datePart = Utilities.formatDate(d, ML_TIMEZONE, 'yyyy-MM-dd'); const timePart = Utilities.formatDate(d, ML_TIMEZONE, 'HH:mm:ss'); if (timePart === '00:00:00') return mlIndianDate_(datePart, !!endOfDay); return d; } const s = String(value).trim(); let m = s.match(/^(\d{1,2})[\/-](\d{1,2})[\/-](\d{4})$/); if (m) return mlIndianDate_([m[3],m[2].padStart(2,'0'),m[1].padStart(2,'0')].join('-'), !!endOfDay); m = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/); if (m) return mlIndianDate_([m[1],m[2].padStart(2,'0'),m[3].padStart(2,'0')].join('-'), !!endOfDay); m = s.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})\s+(\d{1,2}):(\d{2})(?::(\d{2}))?$/); if (m) return mlIndianDate_(`${m[3]}-${m[2].padStart(2,'0')}-${m[1].padStart(2,'0')}`, false, `${m[4].padStart(2,'0')}:${m[5]}:${m[6]||'00'}`); const parsed = new Date(s); // ISO 8601 timestamps with offsets return isNaN(parsed.getTime()) ? null : parsed; } function mlIndianDate_(ymd, endOfDay, time) { const m = ymd.match(/^(\d{4})-(\d{2})-(\d{2})$/); if (!m) return null; const year=+m[1], month=+m[2], day=+m[3]; const valid = new Date(Date.UTC(year,month-1,day)); if (valid.getUTCFullYear() !== year || valid.getUTCMonth() !== month-1 || valid.getUTCDate() !== day) return null; const hm = time || (endOfDay ? '23:59:59' : '00:00:00'); // Indian Standard Time is UTC+05:30 and has no DST. const d = new Date(`${ymd}T${hm}+05:30`); return isNaN(d.getTime()) ? null : d; } function mlJson_(data) { return ContentService.createTextOutput(JSON.stringify(data)) .setMimeType(ContentService.MimeType.JSON); } /** * Diagnostic: run manually in Apps Script editor. No secrets or full emails * are logged. It checks EVERY used row independently from the logged-in user. * Output: Executions > Logs. Does not change any Sheet data. */ function diagnoseMembershipRows() { const book = SpreadsheetApp.getActiveSpreadsheet(); const sheet = book.getSheetByName(ML_MEMBERSHIP_TAB); console.log('Spreadsheet: ' + book.getName()); console.log('Membership tab found: ' + !!sheet); if (!sheet) return; const last=sheet.getLastRow(); console.log('Data rows: ' + Math.max(0,last-1)); if (last < 2) return; const rows=sheet.getRange(2,1,last-1,7).getValues(); const now=Date.now(); rows.forEach((r,i)=>{ const email=String(r[0]||'').trim().toLowerCase(); const amount=Number(String(r[2]||'').replace(/[₹,\s]/g,'')); const status=String(r[5]||'').trim().toLowerCase(); const start=mlAsDate_(r[3],false), end=mlAsDate_(r[4],true); const utr=String(r[6]||'').trim(); const reasons=[]; if (!email) reasons.push('missing email'); if (![9,49].includes(amount)) reasons.push('invalid amount'); if (status!=='active') reasons.push('status not Active'); if (!start) reasons.push('invalid start date'); if (!end) reasons.push('invalid expiry date'); if (!utr) reasons.push('missing UTR'); if (start && start.getTime()>now) reasons.push('not started'); if (end && end.getTime()<=now) reasons.push('expired'); const masked = email ? email.slice(0,2)+'***@'+(email.split('@')[1]||'') : '(empty)'; console.log('Row '+(i+2)+' | '+masked+' | amount='+amount+' | status='+status+ ' | expiry='+(end?Utilities.formatDate(end,ML_TIMEZONE,'yyyy-MM-dd HH:mm:ss'):'invalid')+ ' | '+(reasons.length?reasons.join(', '):'VALID ACTIVE MEMBERSHIP')); }); }