/** * HELKEMA 支援者・入金管理 WEB申込基盤 Version 1.3.0(V1.2.1互換) * Apps Script追加先ファイル名:MemberApplication.gs * * この版で行うこと: * - 「WEB申込」シートがなければ新規作成 * - 見出し、表示形式、入力規則を設定 * - 既存のPAY.JP同期、自動同期、既存データは変更しない * * この版で行わないこと: * - PAY.JPへの通信・決済 * - メール送信 * - WordPress公開 */ const HELKEMA_MEMBER_APPLICATION_VERSION_V120 = '1.2.1'; const SHEET_MEMBER_APPLICATIONS_V120 = 'WEB申込'; const MEMBER_APPLICATION_HEADERS_V120 = [ '申込ID', '受付日時', '支援者ID', '支援方法', '個人・法人', '氏名・法人名', 'ふりがな', '担当者名', '郵便番号', '住所', '電話番号', 'メール', '申込口数', '金額', '継続区分', '支払方法', '申込状態', '決済状態', '入金期限', 'PAY.JP顧客ID', 'PAY.JP支払いID', 'PAY.JP定期課金ID', '規定同意日時', '個人情報同意日時', 'お知らせ受取', '申込元', '備考', 'エラー内容', '最終更新日時', '受付キー' ]; /** * 最初に1回だけ実行します。 * 既存シートがある場合は見出しを診断し、データを上書きしません。 */ function setupMemberApplicationV120() { const ss = SpreadsheetApp.getActiveSpreadsheet(); let sheet = ss.getSheetByName(SHEET_MEMBER_APPLICATIONS_V120); let created = false; if (!sheet) { sheet = ss.insertSheet(SHEET_MEMBER_APPLICATIONS_V120); created = true; buildMemberApplicationSheetV120_(sheet); } else { upgradeMemberApplicationSheetV121_(sheet); assertMemberApplicationHeadersV120_(sheet); applyMemberApplicationFormatsV120_(sheet); } SpreadsheetApp.flush(); const result = { ok: true, version: HELKEMA_MEMBER_APPLICATION_VERSION_V120, sheet: SHEET_MEMBER_APPLICATIONS_V120, created: created, payjpCommunication: false, existingSheetsChanged: false, message: created ? 'WEB申込シートを新規作成しました。' : '既存のWEB申込シートを確認しました。データは上書きしていません。' }; console.log(JSON.stringify(result)); ss.toast(result.message, 'HELKEMA支援者管理', 6); return result; } /** * Version 1.2.1 動作確認用のテスト申込を1件登録する。 * 同じ受付キーが既にある場合は重複登録しない。 */ function testRegisterMemberApplicationV121() { setupMemberApplicationV120(); const result = saveMemberApplicationV121_({ requestKey: 'TEST-MEMBER-APPLICATION-V121', supportType: '個人賛助会員', personType: '個人', name: '【TEST】WEB申込確認', kana: 'てすと うぇぶもうしこみかくにん', postalCode: '000-0000', address: 'テストデータ(実在しない住所)', phone: '000-0000-0000', email: 'test-member@example.invalid', units: 1, continuation: '年次', paymentMethod: '銀行振込', newsletter: '希望しない', source: 'テスト', memo: 'Version 1.2.1 WEB申込登録テスト・集計対象外' }); console.log(JSON.stringify(result)); return result; } /** * Version 1.2.1で作成したテスト申込だけを削除する。 * 受付キーがTEST-で始まり、申込元がテストの行だけが対象。 */ function deleteMemberApplicationTestsV121() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(SHEET_MEMBER_APPLICATIONS_V120); if (!sheet || sheet.getLastRow() < 4) { const emptyResult = { ok: true, deleted: 0, message: '削除対象のテスト申込はありません。' }; console.log(JSON.stringify(emptyResult)); return emptyResult; } const headers = getMemberApplicationHeadersV121_(sheet); const sourceIndex = headerIndexV120_(headers, '申込元'); const requestKeyIndex = headerIndexV120_(headers, '受付キー'); const rows = sheet.getRange(4, 1, sheet.getLastRow() - 3, headers.length).getDisplayValues(); const deleteRows = []; rows.forEach(function(row, index) { const source = String(row[sourceIndex] || '').trim(); const requestKey = String(row[requestKeyIndex] || '').trim(); if (source === 'テスト' && requestKey.indexOf('TEST-') === 0) { deleteRows.push(index + 4); } }); deleteRows.sort(function(a, b) { return b - a; }).forEach(function(rowNo) { sheet.deleteRow(rowNo); }); const result = { ok: true, deleted: deleteRows.length, message: deleteRows.length ? 'テスト申込だけを削除しました。' : '削除対象のテスト申込はありません。' }; console.log(JSON.stringify(result)); return result; } /** WEB申込を重複防止付きで保存する内部処理。 */ function saveMemberApplicationV121_(form) { const lock = LockService.getScriptLock(); if (!lock.tryLock(30000)) { throw new Error('別の申込処理が実行中です。しばらくしてから再実行してください。'); } try { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_MEMBER_APPLICATIONS_V120); if (!sheet) throw new Error('「WEB申込」シートが見つかりません。'); const headers = getMemberApplicationHeadersV121_(sheet); const requestKey = sanitizeMemberTextV121_(form && form.requestKey, 100); if (!requestKey) throw new Error('受付キーがありません。'); const existingRow = findMemberApplicationByRequestKeyV121_(sheet, headers, requestKey); if (existingRow) { return { ok: true, duplicatePrevented: true, row: existingRow, applicationId: String(sheet.getRange(existingRow, headerIndexV120_(headers, '申込ID') + 1).getDisplayValue() || ''), message: '同じ受付キーの申込があるため、二重登録を防止しました。' }; } const supportType = String(form.supportType || '').trim(); const units = Math.max(1, Math.floor(Number(form.units || 1))); const amount = calculateMemberApplicationAmountV121_(supportType, units, form.amount); const now = new Date(); const applicationId = newMemberApplicationIdV121_(now); const paymentMethod = String(form.paymentMethod || '').trim(); // API内部値は PAY.JP のまま保持し、WEB申込シートでは // 入力規則に合わせて「クレジットカード」として保存する。 const sheetPaymentMethod = paymentMethod === 'PAY.JP' ? 'クレジットカード' : paymentMethod; const dueDate = paymentMethod === '銀行振込' ? new Date(now.getFullYear(), now.getMonth(), now.getDate() + 7) : ''; const values = { '申込ID': applicationId, '受付日時': now, '支援者ID': '', '支援方法': supportType, '個人・法人': sanitizeMemberTextV121_(form.personType, 10), '氏名・法人名': sanitizeMemberTextV121_(form.name, 200), 'ふりがな': sanitizeMemberTextV121_(form.kana, 200), '担当者名': sanitizeMemberTextV121_(form.contactName, 200), '郵便番号': sanitizeMemberTextV121_(form.postalCode, 20), '住所': sanitizeMemberTextV121_(form.address, 300), '電話番号': sanitizeMemberTextV121_(form.phone, 40), 'メール': normalizeMemberEmailV121_(form.email), '申込口数': units, '金額': amount, '継続区分': sanitizeMemberTextV121_(form.continuation, 20), '支払方法': sheetPaymentMethod, '申込状態': paymentMethod === '銀行振込' ? '決済待ち' : '申込済', '決済状態': '未決済', '入金期限': dueDate, 'PAY.JP顧客ID': '', 'PAY.JP支払いID': '', 'PAY.JP定期課金ID': '', '規定同意日時': now, '個人情報同意日時': now, 'お知らせ受取': sanitizeMemberTextV121_(form.newsletter, 20), '申込元': sanitizeMemberTextV121_(form.source || 'WEB', 20), '備考': sanitizeMemberTextV121_(form.memo, 500), 'エラー内容': '', '最終更新日時': now, '受付キー': requestKey }; const rowNo = Math.max(4, sheet.getLastRow() + 1); const row = headers.map(function(header) { return Object.prototype.hasOwnProperty.call(values, header) ? values[header] : ''; }); sheet.getRange(rowNo, 1, 1, headers.length).setValues([row]); SpreadsheetApp.flush(); return { ok: true, duplicatePrevented: false, row: rowNo, applicationId: applicationId, amount: amount, message: 'テスト申込を登録しました。' }; } finally { lock.releaseLock(); } } /** 1.2.0の29列構成を、既存データを変えず30列構成へ更新する。 */ function upgradeMemberApplicationSheetV121_(sheet) { const legacyHeaders = MEMBER_APPLICATION_HEADERS_V120.slice(0, 29); if (sheet.getMaxColumns() < MEMBER_APPLICATION_HEADERS_V120.length) { sheet.insertColumnsAfter( sheet.getMaxColumns(), MEMBER_APPLICATION_HEADERS_V120.length - sheet.getMaxColumns() ); } const actualLegacy = sheet.getRange(3, 1, 1, legacyHeaders.length).getDisplayValues()[0] .map(function(value) { return String(value || '').trim(); }); const legacyMatches = legacyHeaders.every(function(header, index) { return actualLegacy[index] === header; }); const lastHeaderCell = sheet.getRange(3, MEMBER_APPLICATION_HEADERS_V120.length); const currentLastHeader = String(lastHeaderCell.getDisplayValue() || '').trim(); if (legacyMatches && !currentLastHeader) { sheet.getRange(1, 1, 1, sheet.getLastColumn()).breakApart(); sheet.getRange(2, 1, 1, sheet.getLastColumn()).breakApart(); sheet.getRange(1, 1, 1, MEMBER_APPLICATION_HEADERS_V120.length).merge(); sheet.getRange(2, 1, 1, MEMBER_APPLICATION_HEADERS_V120.length).merge(); lastHeaderCell.setValue('受付キー'); } } function findMemberApplicationByRequestKeyV121_(sheet, headers, requestKey) { if (sheet.getLastRow() < 4) return 0; const col = headerIndexV120_(headers, '受付キー') + 1; const found = sheet.getRange(4, col, sheet.getLastRow() - 3, 1) .createTextFinder(requestKey) .matchEntireCell(true) .findNext(); return found ? found.getRow() : 0; } function calculateMemberApplicationAmountV121_(supportType, units, requestedAmount) { if (supportType === '個人賛助会員') return 3000 * units; if (supportType === '法人賛助会員') return 30000 * units; if (supportType === '毎月継続寄付') { const monthly = Number(requestedAmount || 0); if ([1000, 2000, 3000, 5000, 10000].indexOf(monthly) === -1) { throw new Error('毎月継続寄付の金額が正しくありません。'); } return monthly; } if (supportType === '今回のみ寄付') { const donation = Math.floor(Number(requestedAmount || 0)); if (!Number.isFinite(donation) || donation < 1000) { throw new Error('今回のみ寄付は1,000円以上で入力してください。'); } return donation; } throw new Error('支援方法が正しくありません。'); } function newMemberApplicationIdV121_(date) { return 'APP-' + Utilities.formatDate(date, 'Asia/Tokyo', 'yyyyMMdd-HHmmss') + '-' + Utilities.getUuid().replace(/-/g, '').slice(0, 6).toUpperCase(); } function getMemberApplicationHeadersV121_(sheet) { return sheet.getRange(3, 1, 1, MEMBER_APPLICATION_HEADERS_V120.length) .getDisplayValues()[0] .map(function(value) { return String(value || '').trim(); }); } function sanitizeMemberTextV121_(value, maxLength) { return String(value || '') .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, '') .trim() .slice(0, maxLength || 500); } function normalizeMemberEmailV121_(value) { const email = String(value || '').trim().toLowerCase(); if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { throw new Error('メールアドレスの形式が正しくありません。'); } return email; } /** WEB申込シートの読み取り専用診断。 */ function diagnoseMemberApplicationV120() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(SHEET_MEMBER_APPLICATIONS_V120); const result = { ok: false, version: HELKEMA_MEMBER_APPLICATION_VERSION_V120, readOnly: true, sheetExists: Boolean(sheet), payjpCommunication: false }; if (!sheet) { result.message = 'WEB申込シートがありません。setupMemberApplicationV120を実行してください。'; console.log(JSON.stringify(result)); return result; } const actualHeaders = sheet .getRange(3, 1, 1, MEMBER_APPLICATION_HEADERS_V120.length) .getDisplayValues()[0] .map(function(value) { return String(value || '').trim(); }); const differences = MEMBER_APPLICATION_HEADERS_V120.filter(function(header, index) { return actualHeaders[index] !== header; }); result.ok = differences.length === 0; result.headerCount = actualHeaders.filter(Boolean).length; result.expectedHeaderCount = MEMBER_APPLICATION_HEADERS_V120.length; result.dataRows = Math.max(0, sheet.getLastRow() - 3); result.differences = differences; result.message = result.ok ? 'WEB申込シートの構成は正常です。' : 'WEB申込シートの見出しに相違があります。'; console.log(JSON.stringify(result)); return result; } /** 新しいWEB申込シートを構築する。 */ function buildMemberApplicationSheetV120_(sheet) { const width = MEMBER_APPLICATION_HEADERS_V120.length; if (sheet.getMaxColumns() < width) { sheet.insertColumnsAfter(sheet.getMaxColumns(), width - sheet.getMaxColumns()); } if (sheet.getMaxRows() < 1000) { sheet.insertRowsAfter(sheet.getMaxRows(), 1000 - sheet.getMaxRows()); } sheet.getRange(1, 1, 1, width).merge(); sheet.getRange(1, 1).setValue('HELKEMA WEB申込'); sheet.getRange(2, 1, 1, width).merge(); sheet.getRange(2, 1).setValue( '賛助会員・継続寄付・今回のみ寄付のWEB申込を一元管理します。カード情報やPAY.JP秘密鍵は保存しません。' ); sheet.getRange(3, 1, 1, width).setValues([MEMBER_APPLICATION_HEADERS_V120]); sheet.setFrozenRows(3); applyMemberApplicationFormatsV120_(sheet); } /** 既存シートの見出しを検査する。 */ function assertMemberApplicationHeadersV120_(sheet) { if (sheet.getLastRow() < 3) { throw new Error( '既存の「WEB申込」シートは構成途中です。安全のため自動上書きしません。シート内容を確認してください。' ); } const actual = sheet .getRange(3, 1, 1, MEMBER_APPLICATION_HEADERS_V120.length) .getDisplayValues()[0] .map(function(value) { return String(value || '').trim(); }); const differences = MEMBER_APPLICATION_HEADERS_V120.filter(function(header, index) { return actual[index] !== header; }); if (differences.length) { throw new Error( '既存の「WEB申込」シートと予定見出しが異なります。安全のため変更しませんでした。相違:' + differences.join('、') ); } } /** 表示形式と入力規則を設定する。 */ function applyMemberApplicationFormatsV120_(sheet) { const headers = MEMBER_APPLICATION_HEADERS_V120; const rowCount = Math.max(1, sheet.getMaxRows() - 3); sheet.getRange(1, 1, 1, headers.length) .setBackground('#2f665a') .setFontColor('#ffffff') .setFontWeight('bold') .setFontSize(16) .setVerticalAlignment('middle'); sheet.setRowHeight(1, 36); sheet.getRange(2, 1, 1, headers.length) .setBackground('#eaf4f2') .setFontColor('#425b55'); sheet.getRange(3, 1, 1, headers.length) .setBackground('#176b87') .setFontColor('#ffffff') .setFontWeight('bold') .setHorizontalAlignment('center') .setWrap(true); sheet.setRowHeight(3, 44); setColumnWidthByHeaderV120_(sheet, headers, '申込ID', 165); setColumnWidthByHeaderV120_(sheet, headers, '受付日時', 145); setColumnWidthByHeaderV120_(sheet, headers, '氏名・法人名', 190); setColumnWidthByHeaderV120_(sheet, headers, '住所', 260); setColumnWidthByHeaderV120_(sheet, headers, 'メール', 220); setColumnWidthByHeaderV120_(sheet, headers, '備考', 260); setColumnWidthByHeaderV120_(sheet, headers, 'エラー内容', 260); ['受付日時', '規定同意日時', '個人情報同意日時', '最終更新日時'].forEach(function(header) { sheet.getRange(4, headerIndexV120_(headers, header) + 1, rowCount, 1) .setNumberFormat('yyyy/MM/dd HH:mm:ss'); }); sheet.getRange(4, headerIndexV120_(headers, '入金期限') + 1, rowCount, 1) .setNumberFormat('yyyy/MM/dd'); sheet.getRange(4, headerIndexV120_(headers, '金額') + 1, rowCount, 1) .setNumberFormat('¥#,##0'); sheet.getRange(4, headerIndexV120_(headers, '申込口数') + 1, rowCount, 1) .setNumberFormat('0'); setListValidationV120_(sheet, headers, '支援方法', [ '個人賛助会員', '法人賛助会員', '毎月継続寄付', '今回のみ寄付' ]); setListValidationV120_(sheet, headers, '個人・法人', ['個人', '法人']); setListValidationV120_(sheet, headers, '継続区分', ['年次', '毎月', '今回のみ']); setListValidationV120_(sheet, headers, '支払方法', ['クレジットカード', '銀行振込']); setListValidationV120_(sheet, headers, '申込状態', [ '入力中', '申込済', '決済待ち', '完了', '決済失敗', '取消' ]); setListValidationV120_(sheet, headers, '決済状態', [ '未決済', '処理中', '成功', '失敗', '返金済み', '対象外' ]); setListValidationV120_(sheet, headers, 'お知らせ受取', ['希望する', '希望しない']); setListValidationV120_(sheet, headers, '申込元', ['WEB', '紙', '電話', '事務局入力', 'テスト']); if (!sheet.getFilter()) { sheet.getRange(3, 1, Math.max(2, sheet.getMaxRows() - 2), headers.length).createFilter(); } } function setListValidationV120_(sheet, headers, header, values) { const rowCount = Math.max(1, sheet.getMaxRows() - 3); const rule = SpreadsheetApp.newDataValidation() .requireValueInList(values, true) .setAllowInvalid(false) .build(); sheet.getRange(4, headerIndexV120_(headers, header) + 1, rowCount, 1) .setDataValidation(rule); } function setColumnWidthByHeaderV120_(sheet, headers, header, width) { sheet.setColumnWidth(headerIndexV120_(headers, header) + 1, width); } function headerIndexV120_(headers, header) { const index = headers.indexOf(header); if (index < 0) throw new Error('列「' + header + '」が見つかりません。'); return index; } /* ===================================================================== * Version 1.3.0 * WEB申込 → 支援者台帳 登録基盤 * * - 既存 Version 1.2.1 の関数名は変更しない(MemberApplicationApi.gs互換維持) * - PAY.JP APIへの通信・決済は行わない * - PAY.JP関連列は新規登録時に変更しない * - 「入会日」は受付日の初期値として1回だけ設定し、その後は自動上書きしない * - WEB申込の「支援者ID」が空欄の行だけを対象にする * - 同一人物の安全な一致(メール+氏名)が1件だけある場合は既存支援者へ紐付ける * - まずは手動実行で検証する。API受付直後の自動登録はこの版では行わない * ===================================================================== */ const HELKEMA_MEMBER_SUPPORTER_IMPORT_VERSION_V130 = '1.3.0'; const SHEET_SUPPORTERS_V130 = '支援者台帳'; const SUPPORTER_REQUIRED_HEADERS_V130 = [ '支援者ID','区分','氏名・法人名','ふりがな','担当者名','メール','電話番号', '郵便番号','住所','入会日','状態','支援区分','会員種別','口数','標準会費', '支払方法','PAY.JP顧客ID','PAY.JP定期課金ID','備考' ]; function diagnoseMemberSupporterImportV130() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const webSheet = ss.getSheetByName(SHEET_MEMBER_APPLICATIONS_V120); const supporterSheet = ss.getSheetByName(SHEET_SUPPORTERS_V130); const result = { ok:false, version:HELKEMA_MEMBER_SUPPORTER_IMPORT_VERSION_V130, readOnly:true, webSheetExists:Boolean(webSheet), supporterSheetExists:Boolean(supporterSheet), payjpCommunication:false, supporterRowsChanged:false, webRowsChanged:false, pendingWebRows:0, webHeaderDifferences:[], supporterHeaderDifferences:[] }; if (!webSheet || !supporterSheet) { result.message = !webSheet ? '「WEB申込」シートが見つかりません。' : '「支援者台帳」シートが見つかりません。'; console.log(JSON.stringify(result)); return result; } const webHeaders = getMemberApplicationHeadersV121_(webSheet); result.webHeaderDifferences = MEMBER_APPLICATION_HEADERS_V120.filter(function(h){ return webHeaders.indexOf(h) === -1; }); const supporterHeaders = getSupporterHeadersV130_(supporterSheet); result.supporterHeaderDifferences = SUPPORTER_REQUIRED_HEADERS_V130.filter(function(h){ return supporterHeaders.indexOf(h) === -1; }); if (!result.webHeaderDifferences.length && !result.supporterHeaderDifferences.length) { result.pendingWebRows = getPendingMemberApplicationRowsV130_(webSheet, webHeaders).length; result.ok = true; result.message = 'WEB申込→支援者台帳の登録基盤は正常です。未登録WEB申込は' + result.pendingWebRows + '件です。'; } else { result.message = '必要な見出しに相違があります。シート構成を確認してください。'; } console.log(JSON.stringify(result)); return result; } function previewPendingMemberSupporterImportsV130() { const diagnosis = diagnoseMemberSupporterImportV130(); if (!diagnosis.ok) throw new Error(diagnosis.message); const ss = SpreadsheetApp.getActiveSpreadsheet(); const webSheet = ss.getSheetByName(SHEET_MEMBER_APPLICATIONS_V120); const headers = getMemberApplicationHeadersV121_(webSheet); const rows = getPendingMemberApplicationRowsV130_(webSheet, headers); const preview = rows.map(function(item) { return { row:item.rowNo, applicationId:memberApplicationValueV130_(item.row, headers, '申込ID'), receivedAt:memberApplicationValueV130_(item.row, headers, '受付日時'), supportType:memberApplicationValueV130_(item.row, headers, '支援方法'), personType:memberApplicationValueV130_(item.row, headers, '個人・法人'), name:memberApplicationValueV130_(item.row, headers, '氏名・法人名'), email:memberApplicationValueV130_(item.row, headers, 'メール'), source:memberApplicationValueV130_(item.row, headers, '申込元') }; }); const result = { ok:true, version:HELKEMA_MEMBER_SUPPORTER_IMPORT_VERSION_V130, readOnly:true, count:preview.length, rows:preview, message:'未登録WEB申込を読み取り専用で確認しました。' }; console.log(JSON.stringify(result)); return result; } function importPendingWebApplicationsToSupportersV130() { return importPendingWebApplicationsToSupportersInternalV130_({ includeTest:false, maxRows:100 }); } function testImportLatestWebApplicationToSupporterV130() { return importPendingWebApplicationsToSupportersInternalV130_({ includeTest:true, testOnly:true, maxRows:1 }); } function deleteMemberSupporterImportTestsV130() { const lock = LockService.getScriptLock(); if (!lock.tryLock(30000)) throw new Error('別の処理が実行中です。しばらくしてから再実行してください。'); try { const ss = SpreadsheetApp.getActiveSpreadsheet(); const supporterSheet = ss.getSheetByName(SHEET_SUPPORTERS_V130); const webSheet = ss.getSheetByName(SHEET_MEMBER_APPLICATIONS_V120); if (!supporterSheet || !webSheet) throw new Error('「支援者台帳」または「WEB申込」シートが見つかりません。'); const supporterHeaders = getSupporterHeadersV130_(supporterSheet); const webHeaders = getMemberApplicationHeadersV121_(webSheet); const sid = headerIndexV130_(supporterHeaders, '支援者ID'); const sname = headerIndexV130_(supporterHeaders, '氏名・法人名'); const smemo = headerIndexV130_(supporterHeaders, '備考'); const deleteRows = []; const deletedIds = []; if (supporterSheet.getLastRow() >= 4) { const values = supporterSheet.getRange(4,1,supporterSheet.getLastRow()-3,supporterHeaders.length).getDisplayValues(); values.forEach(function(row,index) { const name = String(row[sname] || '').trim(); const memo = String(row[smemo] || '').trim(); if (isTestMemberNameV130_(name) && memo.indexOf('WEB申込ID:') !== -1) { const id = String(row[sid] || '').trim(); if (id) deletedIds.push(id); deleteRows.push(index + 4); } }); } deleteRows.sort(function(a,b){ return b-a; }).forEach(function(rowNo){ supporterSheet.deleteRow(rowNo); }); let cleared = 0; if (deletedIds.length && webSheet.getLastRow() >= 4) { const idCol = headerIndexV120_(webHeaders, '支援者ID') + 1; const nameCol = headerIndexV120_(webHeaders, '氏名・法人名') + 1; const count = webSheet.getLastRow() - 3; const ids = webSheet.getRange(4,idCol,count,1).getDisplayValues(); const names = webSheet.getRange(4,nameCol,count,1).getDisplayValues(); for (let i=0; i 1) return {supporterId:'',rowNo:0,ambiguous:true}; const emailMatches = normalizedEmail ? values.filter(function(row){ return String(row[emailIndex] || '').trim().toLowerCase() === normalizedEmail; }) : []; if (emailMatches.length > 1) return {supporterId:'',rowNo:0,ambiguous:true}; return {supporterId:'',rowNo:0,ambiguous:false}; } function resolveSupporterCategoryV130_(supportType,personType,continuation) { const support = String(supportType || '').trim(); const person = String(personType || '').trim(); if (support === '個人賛助会員') { if (person !== '個人') throw new Error('個人賛助会員の個人・法人区分が一致しません。'); return {supportCategory:'賛助会員',memberType:'個人賛助'}; } if (support === '法人賛助会員') { if (person !== '法人') throw new Error('法人賛助会員の個人・法人区分が一致しません。'); return {supportCategory:'賛助会員',memberType:'法人賛助'}; } if (support === '毎月継続寄付' || support === '今回のみ寄付') { return {supportCategory:'寄付者',memberType:''}; } throw new Error('支援方法が正しくありません。'); } function normalizeSupporterPaymentMethodV130_(value) { const text = String(value || '').trim(); // WEB申込では「クレジットカード」、API内部では「PAY.JP」。 // 支援者台帳ではPAY.JPとして統一する。 if (text === 'PAY.JP' || text.indexOf('PAY.JP') === 0 || text === 'クレジットカード') return 'PAY.JP'; if (text === '銀行振込') return '銀行振込'; if (text === '現金') return '現金'; return text; } function startOfJapanDateV130_(value) { let date = value; if (!(date instanceof Date) || isNaN(date.getTime())) date = new Date(value); if (!(date instanceof Date) || isNaN(date.getTime())) throw new Error('受付日時が正しくありません。'); const y = Number(Utilities.formatDate(date,'Asia/Tokyo','yyyy')); const m = Number(Utilities.formatDate(date,'Asia/Tokyo','MM')) - 1; const d = Number(Utilities.formatDate(date,'Asia/Tokyo','dd')); return new Date(y,m,d); } function newMemberSupporterIdV130_() { return 'SUP-' + Utilities.formatDate(new Date(),'Asia/Tokyo','yyyyMMddHHmmss') + '-' + Utilities.getUuid().slice(0,8); } function getSupporterHeadersV130_(sheet) { return sheet.getRange(3,1,1,sheet.getLastColumn()).getDisplayValues()[0].map(function(v){ return String(v || '').trim(); }); } function firstEmptySupporterRowV130_(sheet,headers) { const idCol = headerIndexV130_(headers,'支援者ID') + 1; const lastRow = Math.max(3,sheet.getLastRow()); if (lastRow < 4) return 4; const count = Math.max(1,lastRow-3); const ids = sheet.getRange(4,idCol,count,1).getDisplayValues(); for (let i=0; i= 0) row[index] = values[header] === undefined || values[header] === null ? '' : values[header]; }); sheet.getRange(rowNo,1,1,headers.length).setValues([row]); const admissionIndex = headers.indexOf('入会日'); if (admissionIndex >= 0) sheet.getRange(rowNo,admissionIndex+1).setNumberFormat('yyyy/MM/dd'); const feeIndex = headers.indexOf('標準会費'); if (feeIndex >= 0) sheet.getRange(rowNo,feeIndex+1).setNumberFormat('¥#,##0'); } function setMemberApplicationSupporterIdV130_(sheet,headers,rowNo,supporterId) { sheet.getRange(rowNo,headerIndexV120_(headers,'支援者ID')+1).setValue(supporterId); sheet.getRange(rowNo,headerIndexV120_(headers,'最終更新日時')+1).setValue(new Date()); } function writeMemberApplicationErrorV130_(sheet,headers,rowNo,message) { sheet.getRange(rowNo,headerIndexV120_(headers,'エラー内容')+1).setValue(sanitizeMemberTextV121_(message,500)); sheet.getRange(rowNo,headerIndexV120_(headers,'最終更新日時')+1).setValue(new Date()); } function clearMemberApplicationErrorV130_(sheet,headers,rowNo) { sheet.getRange(rowNo,headerIndexV120_(headers,'エラー内容')+1).clearContent(); sheet.getRange(rowNo,headerIndexV120_(headers,'最終更新日時')+1).setValue(new Date()); } function memberApplicationValueV130_(row,headers,header) { const index = headers.indexOf(header); if (index < 0) throw new Error('WEB申込列「' + header + '」が見つかりません。'); return row[index]; } function requiredMemberApplicationValueV130_(row,headers,header) { const value = memberApplicationValueV130_(row,headers,header); const text = String(value || '').trim(); if (!text) throw new Error(header + 'が空欄です。'); return text; } function normalizeMemberNameV130_(value) { return String(value || '').replace(/[\s ]+/g,'').toLowerCase(); } function isTestMemberNameV130_(value) { const name = String(value || '').trim(); return name.indexOf('[TEST]') === 0 || name.indexOf('【TEST】') === 0; } function headerIndexV130_(headers,header) { const index = headers.indexOf(header); if (index < 0) throw new Error('支援者台帳列「' + header + '」が見つかりません。'); return index; }