import { Injectable } from '@nestjs/common'; import { randomUUID } from 'node:crypto'; import { StructuredLogger } from '../../common/logging/structured-logger.service'; import { TbankAccessService, type TbankSyncAttempt, } from './tbank-access.service'; import { TbankClient } from './tbank.client'; @Injectable() export class TbankMembershipService { private readonly processRunId = randomUUID(); private readonly active = new Map>(); private reservations: Promise = Promise.resolve(); constructor( private readonly access: TbankAccessService, private readonly tbank: TbankClient, private readonly logger: StructuredLogger, ) {} request(userId: string, revision: string, force = false) { const reservation = this.reservations.then(async () => { const requested = await this.access.reserveMembership( userId, revision, this.processRunId, new Set(this.active.keys()), force, ); if (!requested) return null; const { id, attemptId } = requested.work; if (requested.launch) { const done = new Promise((resolve) => setImmediate(resolve)) .then(() => this.execute({ userId, revision, id, attemptId })) .catch(() => this.logger.event('error', 'tbank.membership.persistence_failed', { userId, code: 'TBANK_UNAVAILABLE', }), ) .finally(() => this.active.delete(attemptId)); this.active.set(attemptId, done); } return { id, done: this.active.get(attemptId) ?? Promise.resolve() }; }); this.reservations = reservation.catch(() => undefined); return reservation; } private async execute(attempt: TbankSyncAttempt) { try { const token = await this.access.tokenForAttempt(attempt); if (!token) return; await this.access.publishMembership( attempt, await this.tbank.getOpenAccounts(token), new Date(), true, ); } catch (error) { await this.access.failMembership( attempt, error instanceof Error && error.message === 'INVALID_TBANK_TOKEN', ); } } }