refactor: route bank operation queries through repository (WealthLedger/backend#171)
This commit is contained in:
@@ -4,7 +4,6 @@ import {
|
|||||||
BankCardOperation,
|
BankCardOperation,
|
||||||
Prisma,
|
Prisma,
|
||||||
} from '../../../../../generated/prisma/client';
|
} from '../../../../../generated/prisma/client';
|
||||||
import { PrismaService } from '../../../../prisma/prisma.service';
|
|
||||||
import { FIRST_PAGE } from '../domain/bank-card-limits';
|
import { FIRST_PAGE } from '../domain/bank-card-limits';
|
||||||
import { BankOperationKind } from '../domain/bank-operation-kind';
|
import { BankOperationKind } from '../domain/bank-operation-kind';
|
||||||
import { rubMoney, ZERO_KOPECKS } from '../domain/rub-money';
|
import { rubMoney, ZERO_KOPECKS } from '../domain/rub-money';
|
||||||
@@ -28,15 +27,10 @@ import {
|
|||||||
periodBounds,
|
periodBounds,
|
||||||
} from './bank-card-input';
|
} from './bank-card-input';
|
||||||
|
|
||||||
const EXPECTED_UPDATED_ROWS = 1;
|
|
||||||
|
|
||||||
type Tx = Prisma.TransactionClient;
|
type Tx = Prisma.TransactionClient;
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BankOperationsService {
|
export class BankOperationsService {
|
||||||
constructor(
|
constructor(private readonly repository: BankCardsRepository) {}
|
||||||
private readonly prisma: PrismaService,
|
|
||||||
private readonly repository: BankCardsRepository,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
private async operationView(
|
private async operationView(
|
||||||
tx: Tx,
|
tx: Tx,
|
||||||
@@ -51,21 +45,14 @@ export class BankOperationsService {
|
|||||||
: null;
|
: null;
|
||||||
const categoryId = source?.categoryId ?? row.categoryId;
|
const categoryId = source?.categoryId ?? row.categoryId;
|
||||||
const category = categoryId
|
const category = categoryId
|
||||||
? await tx.expenseCategory.findFirst({
|
? await this.repository.category(tx, row.userId, categoryId)
|
||||||
where: { id: categoryId, userId: row.userId },
|
|
||||||
})
|
|
||||||
: null;
|
: null;
|
||||||
const refunds =
|
const refunds = [] as Awaited<ReturnType<BankCardsRepository['refunds']>>;
|
||||||
row.kind === BankOperationKind.Expense || source
|
if (row.kind === BankOperationKind.Expense || source) {
|
||||||
? await tx.bankCardOperation.findMany({
|
refunds.push(...await this.repository.refunds(
|
||||||
where: {
|
tx, row.userId, source?.id ?? row.id, source?.id ? row.id : undefined,
|
||||||
userId: row.userId,
|
));
|
||||||
originalExpenseId: source?.id ?? row.id,
|
}
|
||||||
deletedAt: null,
|
|
||||||
...(source ? { id: { not: row.id } } : {}),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
kind: row.kind as BankOperationDto['kind'],
|
kind: row.kind as BankOperationDto['kind'],
|
||||||
@@ -94,51 +81,7 @@ export class BankOperationsService {
|
|||||||
private async pageViews(tx: Tx, rows: BankCardOperation[]) {
|
private async pageViews(tx: Tx, rows: BankCardOperation[]) {
|
||||||
const userId = rows[0]?.userId;
|
const userId = rows[0]?.userId;
|
||||||
if (!userId) return [];
|
if (!userId) return [];
|
||||||
const cardIds = new Set(
|
const { cards, sources, categories, refunds } = await this.repository.operationViewData(tx, userId, rows);
|
||||||
rows.flatMap((row) =>
|
|
||||||
[row.cardId, row.targetCardId].filter(
|
|
||||||
(id): id is string => id !== null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const sourceIds = new Set(
|
|
||||||
rows.flatMap((row) => (row.originalExpenseId ? [row.originalExpenseId] : [])),
|
|
||||||
);
|
|
||||||
const [cards, sources] = await Promise.all([
|
|
||||||
tx.bankCard.findMany({ where: { userId, id: { in: [...cardIds] } } }),
|
|
||||||
tx.bankCardOperation.findMany({
|
|
||||||
where: { userId, id: { in: [...sourceIds] } },
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
const categoryIds = new Set(
|
|
||||||
rows
|
|
||||||
.map(
|
|
||||||
(row) =>
|
|
||||||
sources.find((source) => source.id === row.originalExpenseId)
|
|
||||||
?.categoryId ?? row.categoryId,
|
|
||||||
)
|
|
||||||
.filter((id): id is string => id !== null),
|
|
||||||
);
|
|
||||||
const expenseIds = new Set(
|
|
||||||
rows.flatMap((row) => {
|
|
||||||
const source = sources.find((item) => item.id === row.originalExpenseId);
|
|
||||||
return row.kind === BankOperationKind.Expense || source
|
|
||||||
? [source?.id ?? row.id]
|
|
||||||
: [];
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const [categories, refunds] = await Promise.all([
|
|
||||||
tx.expenseCategory.findMany({
|
|
||||||
where: { userId, id: { in: [...categoryIds] } },
|
|
||||||
}),
|
|
||||||
tx.bankCardOperation.findMany({
|
|
||||||
where: {
|
|
||||||
userId,
|
|
||||||
originalExpenseId: { in: [...expenseIds] },
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
const cardsById = new Map(cards.map((card) => [card.id, card]));
|
const cardsById = new Map(cards.map((card) => [card.id, card]));
|
||||||
const sourcesById = new Map(sources.map((source) => [source.id, source]));
|
const sourcesById = new Map(sources.map((source) => [source.id, source]));
|
||||||
const categoriesById = new Map(categories.map((category) => [category.id, category]));
|
const categoriesById = new Map(categories.map((category) => [category.id, category]));
|
||||||
@@ -227,16 +170,12 @@ export class BankOperationsService {
|
|||||||
if (input.kind === BankOperationKind.Expense) {
|
if (input.kind === BankOperationKind.Expense) {
|
||||||
if (input.originalExpenseId) fail();
|
if (input.originalExpenseId) fail();
|
||||||
if (!input.categoryId) {
|
if (!input.categoryId) {
|
||||||
const category = await tx.expenseCategory.findUnique({
|
const category = await this.repository.defaultCategory(tx, userId);
|
||||||
where: { userId_seedKey: { userId, seedKey: 'other' } },
|
|
||||||
});
|
|
||||||
if (!category) missing();
|
if (!category) missing();
|
||||||
input.categoryId = category.id;
|
input.categoryId = category.id;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!(await tx.expenseCategory.findFirst({
|
!(await this.repository.category(tx, userId, input.categoryId))
|
||||||
where: { userId, id: input.categoryId },
|
|
||||||
}))
|
|
||||||
) {
|
) {
|
||||||
missing();
|
missing();
|
||||||
}
|
}
|
||||||
@@ -258,29 +197,11 @@ export class BankOperationsService {
|
|||||||
) {
|
) {
|
||||||
fail(BankErrorCode.InvalidRefund);
|
fail(BankErrorCode.InvalidRefund);
|
||||||
}
|
}
|
||||||
if (
|
await this.repository.updateOperation(tx,
|
||||||
(
|
{ id: source.id, userId, version: source.version, deletedAt: null },
|
||||||
await tx.bankCardOperation.updateMany({
|
{ version: { increment: 1 } },
|
||||||
where: {
|
);
|
||||||
id: source.id,
|
const refunds = await this.repository.refunds(tx, userId, source.id, old?.id);
|
||||||
userId,
|
|
||||||
version: source.version,
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
data: { version: { increment: 1 } },
|
|
||||||
})
|
|
||||||
).count !== EXPECTED_UPDATED_ROWS
|
|
||||||
) {
|
|
||||||
conflict();
|
|
||||||
}
|
|
||||||
const refunds = await tx.bankCardOperation.findMany({
|
|
||||||
where: {
|
|
||||||
userId,
|
|
||||||
originalExpenseId: source.id,
|
|
||||||
deletedAt: null,
|
|
||||||
...(old ? { id: { not: old.id } } : {}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (input.date < source.date) fail(BankErrorCode.InvalidDate);
|
if (input.date < source.date) fail(BankErrorCode.InvalidDate);
|
||||||
if (
|
if (
|
||||||
refunds.reduce((sum, r) => sum + r.amountKopecks, ZERO_KOPECKS) +
|
refunds.reduce((sum, r) => sum + r.amountKopecks, ZERO_KOPECKS) +
|
||||||
@@ -291,9 +212,7 @@ export class BankOperationsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (old?.kind === BankOperationKind.Expense) {
|
if (old?.kind === BankOperationKind.Expense) {
|
||||||
const refunds = await tx.bankCardOperation.findMany({
|
const refunds = await this.repository.refunds(tx, userId, old.id);
|
||||||
where: { userId, originalExpenseId: old.id, deletedAt: null },
|
|
||||||
});
|
|
||||||
if (
|
if (
|
||||||
refunds.length &&
|
refunds.length &&
|
||||||
(input.cardId !== old.cardId ||
|
(input.cardId !== old.cardId ||
|
||||||
@@ -310,9 +229,7 @@ export class BankOperationsService {
|
|||||||
const normalized = operationInput(dto),
|
const normalized = operationInput(dto),
|
||||||
hash = fingerprint(normalized);
|
hash = fingerprint(normalized);
|
||||||
return this.repository.write(async (tx) => {
|
return this.repository.write(async (tx) => {
|
||||||
const existing = await tx.bankCardOperation.findUnique({
|
const existing = await this.repository.operationById(tx, dto.id);
|
||||||
where: { id: dto.id },
|
|
||||||
});
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
if (existing.userId !== userId) missing();
|
if (existing.userId !== userId) missing();
|
||||||
// eslint-disable-next-line security/detect-possible-timing-attacks -- This compares an idempotency fingerprint after ownership validation, not a secret.
|
// eslint-disable-next-line security/detect-possible-timing-attacks -- This compares an idempotency fingerprint after ownership validation, not a secret.
|
||||||
@@ -328,15 +245,15 @@ export class BankOperationsService {
|
|||||||
await this.validateOperation(tx, userId, input);
|
await this.validateOperation(tx, userId, input);
|
||||||
return this.operationView(
|
return this.operationView(
|
||||||
tx,
|
tx,
|
||||||
await tx.bankCardOperation.create({
|
await this.repository.createOperation(tx, {
|
||||||
data: { ...input, id: dto.id, userId, createFingerprint: hash },
|
...input, id: dto.id, userId, createFingerprint: hash,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getOperation(userId: string, id: string) {
|
getOperation(userId: string, id: string) {
|
||||||
return this.prisma.$transaction(async (tx) => {
|
return this.repository.read(async (tx) => {
|
||||||
const row = await this.repository.operation(tx, userId, id);
|
const row = await this.repository.operation(tx, userId, id);
|
||||||
if (row.deletedAt) missing();
|
if (row.deletedAt) missing();
|
||||||
return this.operationView(tx, row);
|
return this.operationView(tx, row);
|
||||||
@@ -361,16 +278,10 @@ export class BankOperationsService {
|
|||||||
description: dto.description ?? old.description ?? undefined,
|
description: dto.description ?? old.description ?? undefined,
|
||||||
});
|
});
|
||||||
await this.validateOperation(tx, userId, input, old);
|
await this.validateOperation(tx, userId, input, old);
|
||||||
if (
|
await this.repository.updateOperation(tx,
|
||||||
(
|
{ id, userId, version: dto.version, deletedAt: null },
|
||||||
await tx.bankCardOperation.updateMany({
|
{ ...input, version: { increment: 1 } },
|
||||||
where: { id, userId, version: dto.version, deletedAt: null },
|
);
|
||||||
data: { ...input, version: { increment: 1 } },
|
|
||||||
})
|
|
||||||
).count !== EXPECTED_UPDATED_ROWS
|
|
||||||
) {
|
|
||||||
conflict();
|
|
||||||
}
|
|
||||||
return this.operationView(
|
return this.operationView(
|
||||||
tx,
|
tx,
|
||||||
await this.repository.operation(tx, userId, id),
|
await this.repository.operation(tx, userId, id),
|
||||||
@@ -389,9 +300,7 @@ export class BankOperationsService {
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
row.kind === BankOperationKind.Expense &&
|
row.kind === BankOperationKind.Expense &&
|
||||||
(await tx.bankCardOperation.findFirst({
|
(await this.repository.dependentRefund(tx, userId, id))
|
||||||
where: { userId, originalExpenseId: id, deletedAt: null },
|
|
||||||
}))
|
|
||||||
) {
|
) {
|
||||||
conflict(BankErrorCode.DependentRefunds);
|
conflict(BankErrorCode.DependentRefunds);
|
||||||
}
|
}
|
||||||
@@ -401,27 +310,15 @@ export class BankOperationsService {
|
|||||||
userId,
|
userId,
|
||||||
row.originalExpenseId,
|
row.originalExpenseId,
|
||||||
);
|
);
|
||||||
if (
|
await this.repository.updateOperation(tx,
|
||||||
(
|
{ id: source.id, userId, version: source.version },
|
||||||
await tx.bankCardOperation.updateMany({
|
{ version: { increment: 1 } },
|
||||||
where: { id: source.id, userId, version: source.version },
|
);
|
||||||
data: { version: { increment: 1 } },
|
|
||||||
})
|
|
||||||
).count !== EXPECTED_UPDATED_ROWS
|
|
||||||
) {
|
|
||||||
conflict();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
(
|
|
||||||
await tx.bankCardOperation.updateMany({
|
|
||||||
where: { id, userId, version, deletedAt: null },
|
|
||||||
data: { deletedAt: new Date(), version: { increment: 1 } },
|
|
||||||
})
|
|
||||||
).count !== EXPECTED_UPDATED_ROWS
|
|
||||||
) {
|
|
||||||
conflict();
|
|
||||||
}
|
}
|
||||||
|
await this.repository.updateOperation(tx,
|
||||||
|
{ id, userId, version, deletedAt: null },
|
||||||
|
{ deletedAt: new Date(), version: { increment: 1 } },
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,25 +331,13 @@ export class BankOperationsService {
|
|||||||
const period = expenses
|
const period = expenses
|
||||||
? undefined
|
? undefined
|
||||||
: periodBounds(query as BankOperationsQueryDto);
|
: periodBounds(query as BankOperationsQueryDto);
|
||||||
return this.prisma.$transaction(async (tx) => {
|
return this.repository.read(async (tx) => {
|
||||||
await this.repository.card(tx, userId, id);
|
await this.repository.card(tx, userId, id);
|
||||||
const limit = query.limit ?? DEFAULT_BANK_PAGE_SIZE,
|
const limit = query.limit ?? DEFAULT_BANK_PAGE_SIZE,
|
||||||
page = query.page ?? FIRST_PAGE;
|
page = query.page ?? FIRST_PAGE;
|
||||||
const where: Prisma.BankCardOperationWhereInput = {
|
const { total, rows } = await this.repository.operationPage(
|
||||||
userId,
|
tx, userId, id, period, limit, page,
|
||||||
deletedAt: null,
|
);
|
||||||
OR: [{ cardId: id }, { targetCardId: id }],
|
|
||||||
...(period
|
|
||||||
? { date: { gte: period.from, lte: period.to } }
|
|
||||||
: { kind: BankOperationKind.Expense }),
|
|
||||||
};
|
|
||||||
const total = await tx.bankCardOperation.count({ where });
|
|
||||||
const rows = await tx.bankCardOperation.findMany({
|
|
||||||
where,
|
|
||||||
orderBy: [{ date: 'desc' }, { createdAt: 'desc' }, { id: 'desc' }],
|
|
||||||
skip: (page - FIRST_PAGE) * limit,
|
|
||||||
take: limit,
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
items: await this.pageViews(tx, rows),
|
items: await this.pageViews(tx, rows),
|
||||||
total,
|
total,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
|
|
||||||
import { BankCard, Prisma } from '../../../../../generated/prisma/client';
|
import { BankCard, BankCardOperation, Prisma } from '../../../../../generated/prisma/client';
|
||||||
import { isPrismaErrorCode, PrismaErrorCode } from '../../../../prisma/prisma-error-code';
|
import { isPrismaErrorCode, PrismaErrorCode } from '../../../../prisma/prisma-error-code';
|
||||||
import { PrismaService } from '../../../../prisma/prisma.service';
|
import { PrismaService } from '../../../../prisma/prisma.service';
|
||||||
import {
|
import {
|
||||||
@@ -9,11 +9,13 @@ import {
|
|||||||
conflict,
|
conflict,
|
||||||
missing,
|
missing,
|
||||||
} from '../application/bank-card-input';
|
} from '../application/bank-card-input';
|
||||||
|
import { FIRST_PAGE } from '../domain/bank-card-limits';
|
||||||
import { BankOperationKind } from '../domain/bank-operation-kind';
|
import { BankOperationKind } from '../domain/bank-operation-kind';
|
||||||
|
|
||||||
const INITIAL_RETRY_COUNT = 0;
|
const INITIAL_RETRY_COUNT = 0;
|
||||||
const LAST_SEED_OFFSET = 1;
|
const LAST_SEED_OFFSET = 1;
|
||||||
const EXPECTED_UPDATED_CARDS = 1;
|
const EXPECTED_UPDATED_CARDS = 1;
|
||||||
|
const EXPECTED_UPDATED_OPERATIONS = 1;
|
||||||
const EXPECTED_UPDATED_CATEGORIES = 1;
|
const EXPECTED_UPDATED_CATEGORIES = 1;
|
||||||
const VERSION_INCREMENT = 1;
|
const VERSION_INCREMENT = 1;
|
||||||
|
|
||||||
@@ -57,6 +59,103 @@ export class BankCardsRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
operationById(tx: Tx, id: string) {
|
||||||
|
return tx.bankCardOperation.findUnique({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
createOperation(tx: Tx, data: Prisma.BankCardOperationUncheckedCreateInput) {
|
||||||
|
return tx.bankCardOperation.create({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateOperation(
|
||||||
|
tx: Tx,
|
||||||
|
where: Prisma.BankCardOperationWhereInput,
|
||||||
|
data: Prisma.BankCardOperationUpdateManyMutationInput,
|
||||||
|
) {
|
||||||
|
const result = await tx.bankCardOperation.updateMany({ where, data });
|
||||||
|
if (result.count !== EXPECTED_UPDATED_OPERATIONS) conflict();
|
||||||
|
}
|
||||||
|
|
||||||
|
category(tx: Tx, userId: string, id: string) {
|
||||||
|
return tx.expenseCategory.findFirst({ where: { id, userId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultCategory(tx: Tx, userId: string) {
|
||||||
|
return tx.expenseCategory.findUnique({
|
||||||
|
where: { userId_seedKey: { userId, seedKey: 'other' } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refunds(tx: Tx, userId: string, originalExpenseId: string, excludeId?: string) {
|
||||||
|
return tx.bankCardOperation.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
originalExpenseId,
|
||||||
|
deletedAt: null,
|
||||||
|
...(excludeId ? { id: { not: excludeId } } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dependentRefund(tx: Tx, userId: string, originalExpenseId: string) {
|
||||||
|
return tx.bankCardOperation.findFirst({
|
||||||
|
where: { userId, originalExpenseId, deletedAt: null },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async operationViewData(tx: Tx, userId: string, rows: BankCardOperation[]) {
|
||||||
|
const cardIds = new Set(rows.flatMap((row) =>
|
||||||
|
[row.cardId, row.targetCardId].filter((id): id is string => id !== null),
|
||||||
|
));
|
||||||
|
const sourceIds = new Set(rows.flatMap((row) =>
|
||||||
|
row.originalExpenseId ? [row.originalExpenseId] : [],
|
||||||
|
));
|
||||||
|
const [cards, sources] = await Promise.all([
|
||||||
|
tx.bankCard.findMany({ where: { userId, id: { in: [...cardIds] } } }),
|
||||||
|
tx.bankCardOperation.findMany({ where: { userId, id: { in: [...sourceIds] } } }),
|
||||||
|
]);
|
||||||
|
const categoryIds = new Set(rows.map((row) =>
|
||||||
|
sources.find((source) => source.id === row.originalExpenseId)?.categoryId ?? row.categoryId,
|
||||||
|
).filter((id): id is string => id !== null));
|
||||||
|
const expenseIds = new Set(rows.flatMap((row) => {
|
||||||
|
const source = sources.find((item) => item.id === row.originalExpenseId);
|
||||||
|
return row.kind === BankOperationKind.Expense || source ? [source?.id ?? row.id] : [];
|
||||||
|
}));
|
||||||
|
const [categories, refunds] = await Promise.all([
|
||||||
|
tx.expenseCategory.findMany({ where: { userId, id: { in: [...categoryIds] } } }),
|
||||||
|
tx.bankCardOperation.findMany({
|
||||||
|
where: { userId, originalExpenseId: { in: [...expenseIds] }, deletedAt: null },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
return { cards, sources, categories, refunds };
|
||||||
|
}
|
||||||
|
|
||||||
|
async operationPage(
|
||||||
|
tx: Tx,
|
||||||
|
userId: string,
|
||||||
|
id: string,
|
||||||
|
period: undefined | { from: string; to: string },
|
||||||
|
limit: number,
|
||||||
|
page: number,
|
||||||
|
) {
|
||||||
|
const where: Prisma.BankCardOperationWhereInput = {
|
||||||
|
userId,
|
||||||
|
deletedAt: null,
|
||||||
|
OR: [{ cardId: id }, { targetCardId: id }],
|
||||||
|
...(period
|
||||||
|
? { date: { gte: period.from, lte: period.to } }
|
||||||
|
: { kind: BankOperationKind.Expense }),
|
||||||
|
};
|
||||||
|
const total = await tx.bankCardOperation.count({ where });
|
||||||
|
const rows = await tx.bankCardOperation.findMany({
|
||||||
|
where,
|
||||||
|
orderBy: [{ date: 'desc' }, { createdAt: 'desc' }, { id: 'desc' }],
|
||||||
|
skip: (page - FIRST_PAGE) * limit,
|
||||||
|
take: limit,
|
||||||
|
});
|
||||||
|
return { total, rows };
|
||||||
|
}
|
||||||
|
|
||||||
cardById(tx: Tx, id: string) {
|
cardById(tx: Tx, id: string) {
|
||||||
return tx.bankCard.findUnique({ where: { id } });
|
return tx.bankCard.findUnique({ where: { id } });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user