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:
1
packages/services/src/index.ts
Normal file
1
packages/services/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './payment';
|
||||
20
packages/services/src/payment/errors.ts
Normal file
20
packages/services/src/payment/errors.ts
Normal 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;
|
||||
4
packages/services/src/payment/index.ts
Normal file
4
packages/services/src/payment/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './errors';
|
||||
export * from './types';
|
||||
export * from './repository';
|
||||
export * from './service';
|
||||
61
packages/services/src/payment/repository.ts
Normal file
61
packages/services/src/payment/repository.ts
Normal 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}`)
|
||||
);
|
||||
},
|
||||
};
|
||||
35
packages/services/src/payment/service.ts
Normal file
35
packages/services/src/payment/service.ts
Normal 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,
|
||||
};
|
||||
50
packages/services/src/payment/types.ts
Normal file
50
packages/services/src/payment/types.ts
Normal 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>;
|
||||
};
|
||||
Reference in New Issue
Block a user