Spaces:
Sleeping
Sleeping
| const SHEET_NAME = 'records'; | |
| const SETTINGS_SHEET_NAME = 'rate'; | |
| const SPREADSHEET_ID = '1XfWFOiuikbyLldmR7Fyz4cJiBTWHtlRFh2hpLt_mHdo'; | |
| const SHARED_SECRET = 'CHANGE_ME_TO_A_LONG_RANDOM_TEXT'; | |
| const HEADERS = [ | |
| 'record_id', | |
| 'saved_at', | |
| 'client_1', | |
| 'client_2', | |
| 'client_3', | |
| 'client_label', | |
| 'loan_amount', | |
| 'property_value', | |
| 'input_json', | |
| ]; | |
| const SETTINGS_HEADERS = [ | |
| 'key', | |
| 'updated_at', | |
| 'settings_json', | |
| ]; | |
| function doPost(e) { | |
| try { | |
| const body = JSON.parse((e && e.postData && e.postData.contents) || '{}'); | |
| if (body.token !== SHARED_SECRET) { | |
| return jsonResponse({ ok: false, error: 'Invalid token.' }); | |
| } | |
| const action = String(body.action || ''); | |
| const payload = body.payload || {}; | |
| if (action === 'save') { | |
| return jsonResponse(saveRecord(payload.record || {})); | |
| } | |
| if (action === 'search') { | |
| return jsonResponse(searchRecords(payload.query || '', payload.limit || 20)); | |
| } | |
| if (action === 'load') { | |
| return jsonResponse(loadRecord(payload.record_id || '')); | |
| } | |
| if (action === 'save_settings') { | |
| return jsonResponse(saveSettings(payload.key || 'policy_settings', payload.settings || {})); | |
| } | |
| if (action === 'load_settings') { | |
| return jsonResponse(loadSettings(payload.key || 'policy_settings')); | |
| } | |
| return jsonResponse({ ok: false, error: 'Unknown action.' }); | |
| } catch (err) { | |
| return jsonResponse({ ok: false, error: String(err && err.message ? err.message : err) }); | |
| } | |
| } | |
| function doGet() { | |
| try { | |
| const sheet = recordsSheet(); | |
| return jsonResponse({ | |
| ok: true, | |
| app: 'UMI Google Sheet API', | |
| sheet: sheet.getName(), | |
| rows: Math.max(sheet.getLastRow() - 1, 0), | |
| }); | |
| } catch (err) { | |
| return jsonResponse({ ok: false, error: String(err && err.message ? err.message : err) }); | |
| } | |
| } | |
| function jsonResponse(data) { | |
| return ContentService | |
| .createTextOutput(JSON.stringify(data)) | |
| .setMimeType(ContentService.MimeType.JSON); | |
| } | |
| function recordsSheet() { | |
| const spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID); | |
| let sheet = spreadsheet.getSheetByName(SHEET_NAME); | |
| if (!sheet) { | |
| sheet = spreadsheet.insertSheet(SHEET_NAME); | |
| } | |
| const currentHeaders = sheet.getRange(1, 1, 1, HEADERS.length).getValues()[0]; | |
| const needsHeaders = HEADERS.some((header, index) => currentHeaders[index] !== header); | |
| if (needsHeaders) { | |
| sheet.getRange(1, 1, 1, HEADERS.length).setValues([HEADERS]); | |
| sheet.setFrozenRows(1); | |
| } | |
| return sheet; | |
| } | |
| function settingsSheet() { | |
| const spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID); | |
| let sheet = spreadsheet.getSheetByName(SETTINGS_SHEET_NAME); | |
| if (!sheet) { | |
| sheet = spreadsheet.insertSheet(SETTINGS_SHEET_NAME); | |
| } | |
| const currentHeaders = sheet.getRange(1, 1, 1, SETTINGS_HEADERS.length).getValues()[0]; | |
| const needsHeaders = SETTINGS_HEADERS.some((header, index) => currentHeaders[index] !== header); | |
| if (needsHeaders) { | |
| sheet.getRange(1, 1, 1, SETTINGS_HEADERS.length).setValues([SETTINGS_HEADERS]); | |
| sheet.setFrozenRows(1); | |
| } | |
| return sheet; | |
| } | |
| function saveRecord(record) { | |
| const sheet = recordsSheet(); | |
| const row = HEADERS.map((header) => record[header] == null ? '' : record[header]); | |
| sheet.appendRow(row); | |
| return { ok: true, saved: true, record_id: record.record_id || '' }; | |
| } | |
| function rowsAsObjects() { | |
| const sheet = recordsSheet(); | |
| const values = sheet.getDataRange().getValues(); | |
| if (values.length <= 1) { | |
| return []; | |
| } | |
| return values.slice(1).map((row) => { | |
| const item = {}; | |
| HEADERS.forEach((header, index) => { | |
| item[header] = row[index]; | |
| }); | |
| return item; | |
| }); | |
| } | |
| function searchRecords(query, limit) { | |
| const normalizedQuery = String(query || '').trim().toLowerCase(); | |
| const maxRows = Math.max(1, Math.min(Number(limit) || 20, 50)); | |
| if (!normalizedQuery) { | |
| return { ok: true, records: [] }; | |
| } | |
| const records = rowsAsObjects() | |
| .filter((record) => { | |
| const names = [ | |
| record.client_1, | |
| record.client_2, | |
| record.client_3, | |
| record.client_label, | |
| ].join(' ').toLowerCase(); | |
| return names.indexOf(normalizedQuery) !== -1; | |
| }) | |
| .sort((a, b) => String(b.saved_at || '').localeCompare(String(a.saved_at || ''))) | |
| .slice(0, maxRows); | |
| return { ok: true, records }; | |
| } | |
| function loadRecord(recordId) { | |
| const targetId = String(recordId || '').trim(); | |
| const record = rowsAsObjects().find((item) => String(item.record_id || '').trim() === targetId); | |
| if (!record) { | |
| return { ok: false, error: 'Record not found.' }; | |
| } | |
| return { ok: true, record }; | |
| } | |
| function saveSettings(key, settings) { | |
| const sheet = settingsSheet(); | |
| const targetKey = String(key || 'policy_settings').trim(); | |
| const settingsJson = JSON.stringify(settings || {}); | |
| const updatedAt = new Date().toISOString(); | |
| const values = sheet.getDataRange().getValues(); | |
| let targetRow = 0; | |
| for (let rowIndex = 1; rowIndex < values.length; rowIndex++) { | |
| if (String(values[rowIndex][0] || '').trim() === targetKey) { | |
| targetRow = rowIndex + 1; | |
| break; | |
| } | |
| } | |
| const row = [targetKey, updatedAt, settingsJson]; | |
| if (targetRow) { | |
| sheet.getRange(targetRow, 1, 1, SETTINGS_HEADERS.length).setValues([row]); | |
| } else { | |
| sheet.appendRow(row); | |
| } | |
| saveReadableSettings(settings || {}, updatedAt); | |
| return { ok: true, saved: true, key: targetKey, updated_at: updatedAt }; | |
| } | |
| function loadSettings(key) { | |
| const sheet = settingsSheet(); | |
| const targetKey = String(key || 'policy_settings').trim(); | |
| const values = sheet.getDataRange().getValues(); | |
| for (let rowIndex = values.length - 1; rowIndex >= 1; rowIndex--) { | |
| if (String(values[rowIndex][0] || '').trim() === targetKey) { | |
| const settings = mergeReadableSettings(JSON.parse(String(values[rowIndex][2] || '{}') || '{}')); | |
| return { | |
| ok: true, | |
| found: true, | |
| key: targetKey, | |
| updated_at: String(values[rowIndex][1] || ''), | |
| settings_json: JSON.stringify(settings), | |
| }; | |
| } | |
| } | |
| return { ok: true, found: false, key: targetKey, settings_json: '' }; | |
| } | |
| function replaceSheetRows(sheetName, headers, rows) { | |
| const spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID); | |
| let sheet = spreadsheet.getSheetByName(sheetName); | |
| if (!sheet) { | |
| sheet = spreadsheet.insertSheet(sheetName); | |
| } | |
| sheet.clearContents(); | |
| sheet.getRange(1, 1, 1, headers.length).setValues([headers]); | |
| if (rows.length) { | |
| sheet.getRange(2, 1, rows.length, headers.length).setValues(rows); | |
| } | |
| sheet.setFrozenRows(1); | |
| } | |
| function sheetObjects(sheetName) { | |
| const spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID); | |
| const sheet = spreadsheet.getSheetByName(sheetName); | |
| if (!sheet) { | |
| return []; | |
| } | |
| const values = sheet.getDataRange().getValues(); | |
| if (values.length <= 1) { | |
| return []; | |
| } | |
| const headers = values[0].map((header) => String(header || '').trim()); | |
| return values.slice(1).filter((row) => row.some((value) => String(value || '').trim() !== '')).map((row) => { | |
| const item = {}; | |
| headers.forEach((header, index) => { | |
| item[header] = row[index]; | |
| }); | |
| return item; | |
| }); | |
| } | |
| function valueOrZero(value) { | |
| return value == null || value === '' ? 0 : value; | |
| } | |
| function saveReadableSettings(settings, updatedAt) { | |
| const rateHeaders = ['Lender', 'Source', 'Product', 'KO', 'Offset', 'Floating Rate', 'Fixed Loan 1yr', 'Fixed Loan 18 months', 'Fixed Loan 2yr', 'Fixed Loan 3yr', 'updated_at']; | |
| const rateRows = (settings.rate_matrix || []).map((row) => [ | |
| row.Lender || '', | |
| row.Source || '', | |
| row.Product || '', | |
| row.KO || '', | |
| valueOrZero(row.Offset), | |
| valueOrZero(row['Floating Rate']), | |
| valueOrZero(row['Fixed Loan 1yr']), | |
| valueOrZero(row['Fixed Loan 18 months']), | |
| valueOrZero(row['Fixed Loan 2yr']), | |
| valueOrZero(row['Fixed Loan 3yr']), | |
| updatedAt, | |
| ]); | |
| replaceSheetRows('rate_matrix', rateHeaders, rateRows); | |
| const pepperHeaders = ['Lender', 'Product Class', 'Meaning', 'Floating Rate', 'Fixed Loan 1yr', 'Fixed Loan 18 months', 'Fixed Loan 2yr', 'Fixed Loan 3yr', 'updated_at']; | |
| const pepperRows = (settings.specialist_rates || []).map((row) => [ | |
| row.Lender || '', | |
| row['Product Class'] || '', | |
| row.Meaning || '', | |
| valueOrZero(row['Floating Rate']), | |
| valueOrZero(row['Fixed Loan 1yr']), | |
| valueOrZero(row['Fixed Loan 18 months']), | |
| valueOrZero(row['Fixed Loan 2yr']), | |
| valueOrZero(row['Fixed Loan 3yr']), | |
| updatedAt, | |
| ]); | |
| replaceSheetRows('pepper_rates', pepperHeaders, pepperRows); | |
| const cashbackHeaders = ['Lender', 'Standard_Pct', 'NewBuild_Pct', 'Max_Limit', 'FHL_Fixed', 'FHL_Pct', 'updated_at']; | |
| const cashbackRows = Object.keys(settings.cashback || {}).map((lender) => { | |
| const row = settings.cashback[lender] || {}; | |
| return [lender, valueOrZero(row.Standard_Pct), valueOrZero(row.NewBuild_Pct), valueOrZero(row.Max_Limit), valueOrZero(row.FHL_Fixed), valueOrZero(row.FHL_Pct), updatedAt]; | |
| }); | |
| replaceSheetRows('cashback', cashbackHeaders, cashbackRows); | |
| const lemHeaders = ['Lender', '80.01-85.00%', '85.01-90.00%', '90.01-95.00%', 'updated_at']; | |
| const lemRows = Object.keys(settings.lem || {}).map((lender) => { | |
| const row = settings.lem[lender] || {}; | |
| return [lender, valueOrZero(row['80.01-85.00%']), valueOrZero(row['85.01-90.00%']), valueOrZero(row['90.01-95.00%']), updatedAt]; | |
| }); | |
| replaceSheetRows('lem', lemHeaders, lemRows); | |
| const testHeaders = ['Lender', 'Sheet', 'Cell', 'updated_at']; | |
| const testRows = Object.keys(settings.test_rate_cells || {}).map((lender) => { | |
| const value = settings.test_rate_cells[lender] || []; | |
| return [lender, value[0] || '', value[1] || '', updatedAt]; | |
| }); | |
| replaceSheetRows('test_rate_cells', testHeaders, testRows); | |
| const lvrHeaders = ['Lender', 'Existing', 'Investment', 'New Build', 'Apt', 'Work Visa', 'FHL', 'updated_at']; | |
| const lvrRows = Object.keys(settings.lvr_policy || {}).map((lender) => { | |
| const row = settings.lvr_policy[lender] || {}; | |
| return [lender, row.Existing || '', row.Investment || '', row['New Build'] || '', row.Apt || '', row['Work Visa'] || '', row.FHL || '', updatedAt]; | |
| }); | |
| replaceSheetRows('lvr_policy', lvrHeaders, lvrRows); | |
| } | |
| function mergeReadableSettings(settings) { | |
| const rateRows = sheetObjects('rate_matrix'); | |
| if (rateRows.length) { | |
| settings.rate_matrix = rateRows.map((row) => ({ | |
| Lender: String(row.Lender || '').trim(), | |
| Source: String(row.Source || '').trim(), | |
| Product: String(row.Product || '').trim(), | |
| KO: String(row.KO || '').trim(), | |
| Offset: valueOrZero(row.Offset), | |
| 'Floating Rate': valueOrZero(row['Floating Rate']), | |
| 'Fixed Loan 1yr': valueOrZero(row['Fixed Loan 1yr']), | |
| 'Fixed Loan 18 months': valueOrZero(row['Fixed Loan 18 months']), | |
| 'Fixed Loan 2yr': valueOrZero(row['Fixed Loan 2yr']), | |
| 'Fixed Loan 3yr': valueOrZero(row['Fixed Loan 3yr']), | |
| })).filter((row) => row.Lender && row.Product); | |
| } | |
| const pepperRows = sheetObjects('pepper_rates'); | |
| if (pepperRows.length) { | |
| settings.specialist_rates = pepperRows.map((row) => ({ | |
| Lender: String(row.Lender || '').trim(), | |
| 'Product Class': String(row['Product Class'] || '').trim(), | |
| Meaning: String(row.Meaning || '').trim(), | |
| 'Floating Rate': valueOrZero(row['Floating Rate']), | |
| 'Fixed Loan 1yr': valueOrZero(row['Fixed Loan 1yr']), | |
| 'Fixed Loan 18 months': valueOrZero(row['Fixed Loan 18 months']), | |
| 'Fixed Loan 2yr': valueOrZero(row['Fixed Loan 2yr']), | |
| 'Fixed Loan 3yr': valueOrZero(row['Fixed Loan 3yr']), | |
| })).filter((row) => row.Lender && row['Product Class']); | |
| } | |
| const cashbackRows = sheetObjects('cashback'); | |
| if (cashbackRows.length) { | |
| settings.cashback = {}; | |
| cashbackRows.forEach((row) => { | |
| const lender = String(row.Lender || '').trim(); | |
| if (!lender) return; | |
| settings.cashback[lender] = { | |
| Standard_Pct: valueOrZero(row.Standard_Pct), | |
| NewBuild_Pct: valueOrZero(row.NewBuild_Pct), | |
| Max_Limit: valueOrZero(row.Max_Limit), | |
| FHL_Fixed: valueOrZero(row.FHL_Fixed), | |
| FHL_Pct: valueOrZero(row.FHL_Pct), | |
| }; | |
| }); | |
| } | |
| const lemRows = sheetObjects('lem'); | |
| if (lemRows.length) { | |
| settings.lem = {}; | |
| lemRows.forEach((row) => { | |
| const lender = String(row.Lender || '').trim(); | |
| if (!lender) return; | |
| settings.lem[lender] = { | |
| '80.01-85.00%': valueOrZero(row['80.01-85.00%']), | |
| '85.01-90.00%': valueOrZero(row['85.01-90.00%']), | |
| '90.01-95.00%': valueOrZero(row['90.01-95.00%']), | |
| }; | |
| }); | |
| } | |
| const testRows = sheetObjects('test_rate_cells'); | |
| if (testRows.length) { | |
| settings.test_rate_cells = {}; | |
| testRows.forEach((row) => { | |
| const lender = String(row.Lender || '').trim(); | |
| if (!lender) return; | |
| settings.test_rate_cells[lender] = [String(row.Sheet || '').trim(), String(row.Cell || '').trim()]; | |
| }); | |
| } | |
| const lvrRows = sheetObjects('lvr_policy'); | |
| if (lvrRows.length) { | |
| settings.lvr_policy = {}; | |
| lvrRows.forEach((row) => { | |
| const lender = String(row.Lender || '').trim(); | |
| if (!lender) return; | |
| settings.lvr_policy[lender] = { | |
| Existing: row.Existing || '', | |
| Investment: row.Investment || '', | |
| 'New Build': row['New Build'] || '', | |
| Apt: row.Apt || '', | |
| 'Work Visa': row['Work Visa'] || '', | |
| FHL: row.FHL || '', | |
| }; | |
| }); | |
| } | |
| return settings; | |
| } | |