brf/server/lib/parse_stream.ts
2025-12-13 21:12:08 +01:00

346 lines
9.6 KiB
TypeScript

import type { ReadableStream } from '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 currentEntry: { id: number; description: string }
let currentInvoiceId: number | null
let currentYear = null
const details: Record<string, string> = {}
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 (transaction.accountNumber === 2441 && currentEntry!.description?.includes('Fakturajournal')) {
await trx('invoice').update('amount', Math.abs(transaction.amount)).where('id', invoiceId)
}
}
if (invoiceId! && currentInvoiceId!) {
throw new Error('invoiceId and currentInvoiceId')
}
const transactionId = (
await trx('transaction')
.insert({
entryId: currentEntry!.id,
...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
}
currentEntry = (
await trx('entry')
.insert({
journalId,
financialYearId: currentYear!.id,
...rest,
})
.returning(['id', 'description'])
)[0]
break
}
default:
details[lineType] = splitLine.slice(1).join(' ')
}
}
await trx.commit()
// oxlint-disable-next-line no-console
console.dir(details)
console.info(`DONE!: ${currentYear.startDate} - ${currentYear.endDate}`)
}