feat: implement payment service with error handling and validation

Add new payment service with repository, error handling, and validation
Add exceptions package with database and validation utilities
Update database package with new schema and zod types
Add turbo start script for development
This commit is contained in:
Yuu Ottosoka
2025-07-11 13:54:55 +03:00
parent 49f0385d00
commit 084b100589
23 changed files with 792 additions and 13 deletions

View File

@@ -0,0 +1,20 @@
{
"name": "@repo/services",
"version": "0.0.0",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"@repo/db": "*",
"@repo/exceptions": "*",
"@repo/utils": "*",
"neverthrow": "^8.2.0",
"zod": "^4.0.2"
},
"devDependencies": {
"@repo/typescript-config": "*",
"typescript": "^5.0.0"
}
}

View File

@@ -0,0 +1 @@
export * from './payment';

View File

@@ -0,0 +1,20 @@
import { AppError, createAppError } from '@repo/exceptions';
export enum PaymentErrorCode {
PAYMENT_NOT_FOUND = 'PAYMENT_NOT_FOUND',
PAYMENT_ALREADY_PROCESSED = 'PAYMENT_ALREADY_PROCESSED',
}
export type PaymentError = AppError<PaymentErrorCode>;
export const paymentNotFound = (paymentId: string): PaymentError =>
createAppError<PaymentErrorCode>(
PaymentErrorCode.PAYMENT_NOT_FOUND,
`Payment with id ${paymentId} not found`
) as PaymentError;
export const paymentAlreadyProcessed = (paymentId: string): PaymentError =>
createAppError(
PaymentErrorCode.PAYMENT_ALREADY_PROCESSED,
`Payment ${paymentId} has already been processed`
) as PaymentError;

View File

@@ -0,0 +1,4 @@
export * from './errors';
export * from './types';
export * from './repository';
export * from './service';

View File

@@ -0,0 +1,61 @@
import { PaymentStatus, Prisma, PrismaClientArg, Payment } from '@repo/db';
import { paymentNotFound, PaymentError } from './errors';
import {
DatabaseError,
databaseError,
wrapDbOperation,
wrapDbOperationWithNull,
} from '@repo/exceptions';
import { ResultAsync } from 'neverthrow';
export const PaymentRepository = {
create(
client: PrismaClientArg,
data: Prisma.PaymentUncheckedCreateInput
): ResultAsync<Payment, DatabaseError> {
return wrapDbOperation(
() => client.payment.create({ data }).then((payment: Payment) => payment),
(e) => databaseError('create-error', e as Error)
);
},
findById(
client: PrismaClientArg,
id: string
): ResultAsync<Payment, PaymentError | DatabaseError> {
return wrapDbOperationWithNull<Payment, PaymentError | DatabaseError>(
() => client.payment.findUnique({ where: { id } }),
paymentNotFound(id),
(e) => databaseError('find-by-id', e as Error)
);
},
updateStatus(
client: PrismaClientArg,
id: string,
status: PaymentStatus
): ResultAsync<Payment, DatabaseError> {
return wrapDbOperation(
() =>
client.payment.update({
where: { id },
data: { status },
}),
() => databaseError(id)
);
},
findByUserId(
client: PrismaClientArg,
telegramUserId: string
): ResultAsync<Payment[], DatabaseError> {
return wrapDbOperation(
() =>
client.payment.findMany({
where: { telegramUserId },
orderBy: { createdAt: 'desc' },
}),
() => databaseError(`user-${telegramUserId}`)
);
},
};

View File

@@ -0,0 +1,35 @@
import { ok, err } from 'neverthrow';
import { CreatePaymentInput, CreatePaymentInputSchema } from './types';
import { paymentAlreadyProcessed } from './errors';
import { PaymentRepository } from './repository';
import { Payment, PaymentStatus, PrismaClient } from '@repo/db';
import { safeParse } from '@repo/exceptions';
const validateInput = safeParse(CreatePaymentInputSchema);
export const createPayment = (
client: PrismaClient,
input: CreatePaymentInput
) =>
validateInput(input).map((validatedInput) =>
PaymentRepository.create(client, validatedInput)
);
export const getPaymentById = (client: PrismaClient, id: string) =>
PaymentRepository.findById(client, id);
export const getUserPayments = (client: PrismaClient, telegramUserId: string) =>
PaymentRepository.findByUserId(client, telegramUserId);
export const validatePaymentForProcessing = (payment: Payment) =>
payment.status === PaymentStatus.PENDING
? ok(payment)
: err(paymentAlreadyProcessed(payment.id));
export const PaymentService = {
validateInput,
createPayment,
getPaymentById,
getUserPayments,
validatePaymentForProcessing,
};

View File

@@ -0,0 +1,50 @@
import { Payment, PaymentStatus, PrismaClient } from '@repo/db';
import { z } from 'zod';
import { ResultAsync } from 'neverthrow';
import { PaymentError } from './errors';
export enum Currency {
USD = 'USD',
EUR = 'EUR',
RUB = 'RUB',
XTR = 'XTR',
}
export const CURRENCIES = Object.values(Currency);
export enum PaymentProvider {
YOOKASSA = 'yookassa',
TELEGRAM = 'telegram',
}
export const PAYMENT_PROVIDERS = Object.values(PaymentProvider);
export const CreatePaymentInputSchema = z.object({
telegramUserId: z.uuid(),
amount: z.number().positive(),
currency: z.enum(Currency),
provider: z.enum(PaymentProvider),
});
export type CreatePaymentInput = z.infer<typeof CreatePaymentInputSchema>;
export type PaymentProviderResponse = {
readonly paymentUrl: string;
readonly providerId: string;
};
export type IPaymentProvider = {
readonly createPayment: (
client: PrismaClient,
input: CreatePaymentInput
) => ResultAsync<PaymentProviderResponse, PaymentError>;
readonly verifyPayment: (
client: PrismaClient,
payment: Payment
) => ResultAsync<PaymentStatus, PaymentError>;
readonly refundPayment: (
client: PrismaClient,
providerId: string,
amount?: number
) => ResultAsync<boolean, PaymentError>;
};

View File

@@ -0,0 +1,11 @@
{
"extends": "@repo/typescript-config/base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"declaration": false,
"declarationMap": false
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}