import { type ReadableStream } from 'node:stream/web' import knex from './knex.ts' import split, { type Decoder } from './split.ts' import { parseDim, parseIB, parseKonto, parseObjekt, parseRAR, parseSRU, parseTrans, parseUB, parseVer, } from './parse_line.ts' const defaultDecoder = { decode(chunk: Uint8Array) { return Array.from(chunk, (uint) => { switch (uint) { case 132: return 'ä' case 134: return 'å' case 148: return 'ö' case 142: return 'Ä' case 143: return 'Å' case 153: return 'Ö' default: return String.fromCharCode(uint) } }).join('') }, } export default async function parseStream(stream: ReadableStream, decoder: Decoder = defaultDecoder) { const journals = new Map() let currentEntryId: number let currentInvoiceId: number let currentYear = null const details: Record = {} const trx = await knex.transaction() async function getJournalId(identifier: string) { if (journals.has(identifier)) { return journals.get(identifier) } let journal = await trx('journal').first('*').where('identifier', identifier) if (!journal) { journal = (await trx('journal').insert({ identifier }).returning('*'))[0] } journals.set(identifier, journal.id) return journal.id } for await (let line of stream.pipeThrough(split(null, { decoder }))) { line = line.trim() if (line[0] !== '#') { continue } const splitLine = line.split(/\s+/) const lineType = splitLine[0] switch (lineType) { case '#DIM': { const { number, name } = parseDim(line) const existingDimension = await trx('dimension').first('*').where('number', number) if (!existingDimension) { await trx('dimension').insert({ number, name }) } else if (existingDimension.name !== name) { await trx('dimension').update({ name }).where('number', number) } break } case '#IB': { const { yearNumber, accountNumber, balance, quantity } = parseIB(line) if (yearNumber !== 0) continue const existingAccountBalance = await trx('accountBalance') .first('*') .where({ financialYearId: currentYear.id, accountNumber }) if (!existingAccountBalance) { await trx('accountBalance').insert({ financialYearId: currentYear.id, accountNumber, in: balance, inQuantity: quantity, }) } else { await trx('accountBalance') .update({ in: balance, inQuantity: quantity, }) .where({ financialYearId: currentYear.id, accountNumber, }) } break } case '#KONTO': { const { number, description } = parseKonto(line) const existingAccount = await trx('account') .first('*') .where('number', number) .orderBy('financialYearId', 'desc') if (!existingAccount) { await trx('account').insert({ financialYearId: currentYear!.id, number, description, }) } else if (existingAccount.description !== description) { await trx('account').update({ description }).where('id', existingAccount.id) } break } case '#OBJEKT': { const { dimensionNumber, number, name } = parseObjekt(line) const dimension = await trx('dimension').first('*').where('number', dimensionNumber) if (!dimension) throw new Error(`Dimension "${dimensionNumber}" does not exist`) const existingObject = await trx('object').first('*').where({ dimensionId: dimension.id, number }) if (!existingObject) { await trx('object').insert({ dimensionId: dimension.id, number, name }) } else if (existingObject.name !== name) { await trx('object').update({ name }).where({ dimensionId: dimension.id, number }) } break } case '#RAR': { const { yearNumber, startDate, endDate } = parseRAR(line) if (yearNumber !== 0) continue currentYear = ( await trx('financial_year') .insert({ year: startDate.slice(0, 4), startDate, endDate }) .returning('*') )[0] break } case '#SRU': { const { number, sru } = parseSRU(line) const existingAccount = await trx('account') .first('*') .where('number', number) .orderBy('financialYearId', 'desc') if (existingAccount) { if (existingAccount.sru !== sru) { await trx('account').update({ sru: sru }).where('id', existingAccount.id) } } else { await trx('account').insert({ financialYearId: currentYear!.id, number, description: existingAccount.description, sru, }) } break } case '#TRANS': { const { objectList, ...transaction } = parseTrans(line) // Faktura 6364710056 172-2 - SBAB Bank AB // Faktura 2312 172-6 - Bredablick Frvaltning AB const rFisken = /Faktura.*172-(\d*)\s+-\s+(.+)/ const result = transaction.description?.match(rFisken) let invoiceId: number if (result) { const [, fiskenNumber, supplierName] = result // invoiceId = (await trx('invoice').first('id').where({ fiskenNumber})).id invoiceId = (await trx('invoice').first('id').where({ fiskenNumber }))?.id if (!invoiceId) { let supplierId = (await trx('supplier').first('id').where('name', supplierName))?.id if (!supplierId) { supplierId = (await trx('supplier').insert({ name: supplierName, supplierTypeId: 1 }).returning('id'))[0] ?.id } invoiceId = ( await trx('invoice').insert({ financialYearId: currentYear.id, fiskenNumber, supplierId }).returning('id') )[0]?.id } } if (invoiceId && currentInvoiceId) { throw new Error('invoiceId and currentInvoiceId') } const transactionId = ( await trx('transaction') .insert({ entryId: currentEntryId, ...transaction, invoiceId: invoiceId || currentInvoiceId, }) .returning('id') )[0].id if (objectList) { for (const [dimensionNumber, objectNumber] of objectList) { const objectId = ( await trx('object') .first('object.id') .innerJoin('dimension', 'object.dimension_id', 'dimension.id') .where({ 'object.number': objectNumber, 'dimension.number': dimensionNumber, }) )?.id if (!objectId) { throw new Error(`Object {${dimensionNumber} ${objectNumber}} does not exist!`) } await trx('transactionsToObjects').insert({ transactionId, objectId, }) } } break } case '#UB': { const { yearNumber, accountNumber, balance, quantity } = parseUB(line) if (yearNumber !== 0) continue const existingAccountBalance = await trx('accountBalance') .first('*') .where({ financialYearId: currentYear.id, accountNumber }) if (!existingAccountBalance) { await trx('accountBalance').insert({ financialYearId: currentYear.id, accountNumber, out: balance, outQuantity: quantity, }) } else { await trx('accountBalance') .update({ out: balance, outQuantity: quantity, }) .where({ financialYearId: currentYear.id, accountNumber, }) } break } case '#VER': { const { journal, ...rest } = parseVer(line) let journalId = await getJournalId(journal) // Fisken - Fakturajournal 1248 // Fisken - Utbetalningsjournal 1056 // Levfakt EURO FINANS AB (5) / Levfakt Per Holmberg (6) // Levbet EURO FINANS AB (5) / Levbet Per Holmberg (6) const rPhm = /^Lev(?:fakt|bet)\s+[^(]*\((\d+)\)$/ const result = rest.description.match(rPhm) if (result) { currentInvoiceId = ( await trx('invoice') .select('invoice.*', 'supplier.name') .innerJoin('supplier', 'invoice.supplierId', 'supplier.id') .where('phmNumber', result[1]) )[0]?.id } else { currentInvoiceId = null } currentEntryId = ( await trx('entry') .insert({ journalId, financialYearId: currentYear!.id, ...rest, }) .returning('id') )[0].id break } default: details[lineType] = splitLine.slice(1).join(' ') } } await trx.commit() console.dir(details) console.info(`DONE!: ${currentYear.startDate} - ${currentYear.endDate}`) }