Use owner enums for error and T-Bank protocol values with ESLint regression checks. Preserve open wire fallbacks and keep protocol imports independent of DI and Prisma. Verified npm ci, verify, 226 unit tests, 262 isolated e2e tests and unchanged OpenAPI.
71 lines
2.3 KiB
TypeScript
71 lines
2.3 KiB
TypeScript
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 { isTbankErrorCode, TbankErrorCode } from './tbank-error-code';
|
|
import { TbankClient } from './tbank.client';
|
|
|
|
@Injectable()
|
|
export class TbankMembershipService {
|
|
private readonly processRunId = randomUUID();
|
|
private readonly active = new Map<string, Promise<void>>();
|
|
private reservations: Promise<unknown> = 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<void>((resolve) => setImmediate(resolve))
|
|
.then(() => this.execute({ userId, revision, id, attemptId }))
|
|
.catch(() =>
|
|
this.logger.event('error', 'tbank.membership.persistence_failed', {
|
|
userId,
|
|
code: TbankErrorCode.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 && isTbankErrorCode(error.message, TbankErrorCode.InvalidToken),
|
|
);
|
|
}
|
|
}
|
|
}
|