import { createReadStream } from 'fs'; import { createGunzip } from 'zlib'; import { PrismaClient } from '@prisma/client'; import { parseOaJson } from './oa-json'; const prisma = new PrismaClient(); const DUMP = process.env.OA_DUMP || '/opt/backup/cloud-20260830/oa.sql.gz'; const HR = '/opt/import/prod/oa_hr.json'; function n(v: unknown) { if (v == null || v === '') return 0; const x = Number(v); return Number.isFinite(x) ? x : 0; } function parseMysqlTuples(s: string): unknown[][] { const rows: unknown[][] = []; let i = 0; while (i < s.length) { while (i < s.length && ' \n\r\t,'.includes(s[i])) i += 1; if (i >= s.length) break; if (s[i] !== '(') throw new Error(`expected ( at ${i}`); i += 1; const row: unknown[] = []; while (true) { while (i < s.length && ' \n\r\t'.includes(s[i])) i += 1; if (s.startsWith('NULL', i) && (i + 4 === s.length || ',) \n\r\t'.includes(s[i + 4]))) { row.push(null); i += 4; } else if (s[i] === "'" || s[i] === '"') { const q = s[i]; i += 1; let out = ''; while (i < s.length) { const ch = s[i]; if (ch === '\\') { out += s[i + 1]; i += 2; continue; } if (ch === q) { if (s[i + 1] === q) { out += q; i += 2; continue; } i += 1; break; } out += ch; i += 1; } row.push(out); } else { const start = i; while (i < s.length && s[i] !== ',' && s[i] !== ')') i += 1; const raw = s.slice(start, i).trim(); row.push(raw === '' ? null : /^-?\d+(\.\d+)?$/.test(raw) ? Number(raw) : raw); } while (i < s.length && ' \n\r\t'.includes(s[i])) i += 1; if (s[i] === ',') { i += 1; continue; } if (s[i] === ')') { i += 1; rows.push(row); break; } throw new Error(`bad token at ${i}`); } } return rows; } function insertsFor(dump: string, table: string) { const rows: unknown[][] = []; const needle = `INSERT INTO \`${table}\` VALUES `; let from = 0; while (true) { const start = dump.indexOf(needle, from); if (start < 0) break; let i = start + needle.length; while (i < dump.length) { while (i < dump.length && ' \n\r\t'.includes(dump[i])) i += 1; if (dump[i] === ';') { i += 1; break; } if (dump[i] === ',') { i += 1; continue; } if (dump[i] !== '(') break; const sliceStart = i; let depth = 0; let inStr = false; let q = ''; while (i < dump.length) { const ch = dump[i]; if (inStr) { if (ch === '\\') { i += 2; continue; } if (ch === q) inStr = false; i += 1; continue; } if (ch === "'" || ch === '"') { inStr = true; q = ch; i += 1; continue; } if (ch === '(') depth += 1; if (ch === ')') { depth -= 1; if (depth === 0) { i += 1; break; } } i += 1; } rows.push(...parseMysqlTuples(dump.slice(sliceStart, i))); } from = i; } return rows; } async function readDump() { return new Promise((resolve, reject) => { const chunks: Buffer[] = []; createReadStream(DUMP) .pipe(createGunzip()) .on('data', (c: Buffer) => chunks.push(c)) .on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))) .on('error', reject); }); } async function main() { const dump = await readDump(); const hr = parseOaJson(HR) as { users?: { id: number; nickName?: string; phone?: string }[] }; const employees = await prisma.employee.findMany({ select: { id: true, name: true, mobile: true } }); const byName = new Map(employees.map((e) => [e.name, e])); const byMobile = new Map(employees.filter((e) => e.mobile).map((e) => [e.mobile as string, e])); const oaUserToEmp = new Map(); for (const u of hr.users || []) { const hit = (u.phone && byMobile.get(String(u.phone))) || (u.nickName && byName.get(u.nickName)); if (hit) oaUserToEmp.set(Number(u.id), hit.id); } const months = insertsFor(dump, 'oa_salary_month'); const slips = insertsFor(dump, 'oa_salary_slip'); const admin = await prisma.user.findFirst({ where: { username: '18115604840' } }); await prisma.payrollItem.deleteMany(); await prisma.payrollRun.deleteMany(); const runByYm = new Map(); for (const row of months) { const ym = String(row[1] || ''); if (!/^\d{4}-\d{2}$/.test(ym)) continue; const confirmed = Number(row[2]) === 1; const run = await prisma.payrollRun.create({ data: { tenantId: '1', yearMonth: ym, status: confirmed ? 'CONFIRMED' : 'DRAFT', createdById: admin?.id, confirmedAt: confirmed ? (row[7] ? new Date(String(row[7])) : new Date()) : null, }, }); runByYm.set(ym, run.id); } let ok = 0; let skip = 0; for (const row of slips) { const ym = String(row[2] || ''); const oaUserId = Number(row[3]); const name = String(row[4] || ''); let empId = oaUserToEmp.get(oaUserId); if (!empId && name) empId = byName.get(name)?.id; if (!empId || !runByYm.has(ym)) { skip += 1; continue; } await prisma.payrollItem.upsert({ where: { runId_employeeId: { runId: runByYm.get(ym) as string, employeeId: empId } }, update: { basePay: n(row[9]) + n(row[10]), performancePay: n(row[18] || row[11]), leaveDeduct: n(row[22]), sickDeduct: n(row[23]), socialPersonal: n(row[27]), tax: n(row[28]), grossPay: n(row[16]), netPay: n(row[30]), scheduledDays: n(row[31]), prorationFactor: n(row[29]) || 1, }, create: { runId: runByYm.get(ym) as string, employeeId: empId, basePay: n(row[9]) + n(row[10]), performancePay: n(row[18] || row[11]), leaveDeduct: n(row[22]), sickDeduct: n(row[23]), socialPersonal: n(row[27]), tax: n(row[28]), grossPay: n(row[16]), netPay: n(row[30]), scheduledDays: n(row[31]), prorationFactor: n(row[29]) || 1, }, }); ok += 1; } console.log( JSON.stringify({ months: runByYm.size, slips: slips.length, imported: ok, skipped: skip, }), ); } main() .then(async () => prisma.$disconnect()) .catch(async (e) => { console.error(e); await prisma.$disconnect(); process.exit(1); });