brf/server/lib/parse_stream.ts
2025-12-18 10:20:02 +01:00

426 lines
12 KiB
TypeScript

import type { ReadableStream } from 'stream/web'
import type { Selectable } from 'kysely'
import db from './kysely.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('')
},
}
import type { Entry, FinancialYear } from '../../shared/types.db.ts'
export default async function parseStream(stream: ReadableStream, decoder: Decoder = defaultDecoder) {
const journals = new Map()
let currentEntry: Pick<Selectable<Entry>, 'id' | 'description'>
let currentInvoiceId: number | null | undefined
let currentYear: Selectable<FinancialYear>
const details: Record<string, string> = {}
const trx = await db.startTransaction().execute()
async function getJournalId(identifier: string) {
if (journals.has(identifier)) {
return journals.get(identifier)
}
let journal = await trx.selectFrom('journal').selectAll().where('identifier', '=', identifier).executeTakeFirst()
if (!journal) {
journal = await trx.insertInto('journal').values({ identifier }).returningAll().executeTakeFirstOrThrow()
}
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 db
.selectFrom('dimension')
.selectAll()
.where('number', '=', number)
.executeTakeFirst()
if (!existingDimension) {
await trx.insertInto('dimension').values({ number, name }).execute()
} else if (existingDimension.name !== name) {
await trx.updateTable('dimension').set({ name }).where('number', '=', number).execute()
}
break
}
case '#IB': {
const { yearNumber, accountNumber, balance, quantity } = parseIB(line)
if (yearNumber !== 0) continue
const existingAccountBalance = await trx
.selectFrom('accountBalance')
.selectAll()
.where((eb) => eb.and({ financialYearId: currentYear!.id, accountNumber }))
.executeTakeFirst()
if (!existingAccountBalance) {
await trx
.insertInto('accountBalance')
.values({
financialYearId: currentYear!.id,
accountNumber,
in: balance,
inQuantity: quantity,
})
.execute()
} else {
await trx
.updateTable('accountBalance')
.set({
in: balance,
inQuantity: quantity,
})
.where((eb) =>
eb.and({
financialYearId: currentYear!.id,
accountNumber,
}),
)
.execute()
}
break
}
case '#KONTO': {
const { number, description } = parseKonto(line)
const existingAccount = await trx
.selectFrom('account')
.selectAll()
.where('number', '=', number)
.orderBy('financialYearId', 'desc')
.executeTakeFirst()
if (!existingAccount) {
await trx
.insertInto('account')
.values({
financialYearId: currentYear!.id,
number,
description,
})
.execute()
} else if (existingAccount.description !== description) {
await trx.updateTable('account').set({ description }).where('id', '=', existingAccount.id).execute()
}
break
}
case '#OBJEKT': {
const { dimensionNumber, number, name } = parseObjekt(line)
const dimension = await trx
.selectFrom('dimension')
.selectAll()
.where('number', '=', dimensionNumber)
.executeTakeFirst()
if (!dimension) throw new Error(`Dimension "${dimensionNumber}" does not exist`)
const existingObject = await trx
.selectFrom('object')
.selectAll()
.where((eb) => eb.and({ dimensionId: dimension.id, number }))
.executeTakeFirst()
if (!existingObject) {
await trx.insertInto('object').values({ dimensionId: dimension.id, number, name }).execute()
} else if (existingObject.name !== name) {
await trx
.updateTable('object')
.set({ name })
.where((eb) => eb.and({ dimensionId: dimension.id, number }))
.execute()
}
break
}
case '#RAR': {
const { yearNumber, startDate, endDate } = parseRAR(line)
if (yearNumber !== 0) continue
currentYear = (
await trx
.insertInto('financialYear')
.values({ year: parseInt(startDate.slice(0, 4)), startDate, endDate })
.returningAll()
.execute()
)[0]
break
}
case '#SRU': {
const { number, sru } = parseSRU(line)
const existingAccount = await trx
.selectFrom('account')
.selectAll()
.where('number', '=', number)
.orderBy('financialYearId', 'desc')
.executeTakeFirst()
if (existingAccount) {
if (existingAccount.sru !== sru) {
await trx.updateTable('account').set({ sru: sru }).where('id', '=', existingAccount.id).execute()
}
} else {
await trx
.insertInto('account')
.values({
financialYearId: currentYear!.id,
number,
description: existingAccount!.description,
sru,
})
.execute()
}
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 | null | undefined
if (result) {
const [, fiskenNumber, supplierName] = result
// invoiceId = (await trx('invoice').first('id').where({ fiskenNumber})).id
invoiceId = (
await trx
.selectFrom('invoice')
.select('id')
.where('fiskenNumber', '=', parseInt(fiskenNumber))
.executeTakeFirst()
)?.id
if (!invoiceId) {
let supplierId = (
await trx.selectFrom('supplier').select('id').where('name', '=', supplierName).executeTakeFirst()
)?.id
if (!supplierId) {
supplierId = (
await trx
.insertInto('supplier')
.values({ name: supplierName, supplierTypeId: 1 })
.returning('id')
.executeTakeFirstOrThrow()
).id
}
invoiceId = (
await trx
.insertInto('invoice')
.values({ financialYearId: currentYear!.id, fiskenNumber: parseInt(fiskenNumber), supplierId })
.returning('id')
.executeTakeFirstOrThrow()
).id
}
if (transaction.accountNumber === 2441 && currentEntry!.description?.includes('Fakturajournal')) {
await trx
.updateTable('invoice')
.set('amount', Math.abs(transaction.amount))
.where('id', '=', invoiceId!)
.execute()
}
}
if (invoiceId && currentInvoiceId) {
throw new Error('invoiceId and currentInvoiceId')
}
const transactionId = (
await trx
.insertInto('transaction')
.values({
entryId: currentEntry!.id,
...transaction,
invoiceId: invoiceId! || currentInvoiceId!,
})
.returning('id')
.executeTakeFirstOrThrow()
).id
if (objectList) {
for (const [dimensionNumber, objectNumber] of objectList) {
const objectId = (
await trx
.selectFrom('object')
.innerJoin('dimension', 'object.dimensionId', 'dimension.id')
.select('object.id')
.where((eb) =>
eb.and({
'object.number': objectNumber,
'dimension.number': dimensionNumber,
}),
)
.executeTakeFirst()
)?.id
if (!objectId) {
throw new Error(`Object {${dimensionNumber} ${objectNumber}} does not exist!`)
}
await trx
.insertInto('transactionsToObjects')
.values({
transactionId,
objectId,
})
.execute()
}
}
break
}
case '#UB': {
const { yearNumber, accountNumber, balance, quantity } = parseUB(line)
if (yearNumber !== 0) continue
const existingAccountBalance = await trx
.selectFrom('accountBalance')
.selectAll()
.where((eb) => eb.and({ financialYearId: currentYear!.id, accountNumber }))
.executeTakeFirst()
if (!existingAccountBalance) {
await trx
.insertInto('accountBalance')
.values({
financialYearId: currentYear!.id,
accountNumber,
out: balance,
outQuantity: quantity,
})
.execute()
} else {
await trx
.updateTable('accountBalance')
.set({
out: balance,
outQuantity: quantity,
})
.where((eb) =>
eb.and({
financialYearId: currentYear!.id,
accountNumber,
}),
)
.execute()
}
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
.selectFrom('invoice')
.innerJoin('supplier', 'invoice.supplierId', 'supplier.id')
.selectAll('invoice')
.select('supplier.name')
.where('phmNumber', '=', parseInt(result[1]))
.executeTakeFirst()
)?.id
} else {
currentInvoiceId = null
}
currentEntry = await trx
.insertInto('entry')
.values({
journalId,
financialYearId: currentYear!.id,
...rest,
})
.returning(['id', 'description'])
.executeTakeFirstOrThrow()
break
}
default:
details[lineType] = splitLine.slice(1).join(' ')
}
}
await trx.commit().execute()
// oxlint-disable-next-line no-console
console.dir(details)
console.info(`DONE!: ${currentYear!.startDate} - ${currentYear!.endDate}`)
}