paymnet methods cards new table

This commit is contained in:
2026-06-28 22:41:50 +03:30
parent a565f93044
commit 604447d3e8
10 changed files with 262 additions and 111 deletions
@@ -1,6 +1,8 @@
import { IsString, IsOptional, IsBoolean, IsNumber, IsEnum, ValidateIf, IsNotEmpty } from 'class-validator';
import { IsString, IsOptional, IsBoolean, IsNumber, IsEnum, ValidateIf, IsArray, ValidateNested, ArrayMinSize } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { PaymentMethodEnum, PaymentGatewayEnum } from '../interface/payment';
import { PaymentMethodCardDto } from './payment-method-card.dto';
export class CreatePaymentMethodDto {
@@ -18,36 +20,13 @@ export class CreatePaymentMethodDto {
@IsString()
description?: string;
@ApiProperty({ description: 'Card number for card-to-card payments', required: false })
@IsOptional()
@IsString()
cardNumber?: string;
@ApiProperty({ description: 'Second card number for card-to-card payments', required: false })
@IsOptional()
@IsString()
cardNumber2?: string;
@ApiProperty({ description: 'Third card number for card-to-card payments', required: false })
@IsOptional()
@IsString()
cardNumber3?: string;
@ApiProperty({ description: 'Card owner name for card-to-card payments', required: false })
@ApiProperty({ description: 'Cards for card-to-card payments', type: [PaymentMethodCardDto], required: false })
@ValidateIf((dto: CreatePaymentMethodDto) => dto.method === PaymentMethodEnum.CreditCard)
@IsNotEmpty()
@IsString()
cardOwner?: string;
@ApiProperty({ description: 'Second card owner name for card-to-card payments', required: false })
@IsOptional()
@IsString()
cardOwner2?: string;
@ApiProperty({ description: 'Third card owner name for card-to-card payments', required: false })
@IsOptional()
@IsString()
cardOwner3?: string;
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
@Type(() => PaymentMethodCardDto)
cards?: PaymentMethodCardDto[];
@ApiProperty({ description: 'Is payment method enabled', required: false, default: true })
@IsOptional()
@@ -0,0 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class PaymentMethodCardDto {
@ApiProperty({ description: 'Card number for card-to-card payments' })
@IsNotEmpty()
@IsString()
cardNumber!: string;
@ApiProperty({ description: 'Card owner name for card-to-card payments' })
@IsNotEmpty()
@IsString()
owner!: string;
}
@@ -0,0 +1,16 @@
import { Entity, Index, ManyToOne, Property } from '@mikro-orm/core';
import { BaseEntity } from '../../../common/entities/base.entity';
import { PaymentMethod } from './payment-method.entity';
@Entity({ tableName: 'payment_method_cards' })
@Index({ properties: ['paymentMethod'] })
export class PaymentMethodCard extends BaseEntity {
@ManyToOne(() => PaymentMethod)
paymentMethod!: PaymentMethod;
@Property()
cardNumber!: string;
@Property()
owner!: string;
}
@@ -1,7 +1,8 @@
import { Entity, Enum, Index, ManyToOne, Property, Unique } from '@mikro-orm/core';
import { Cascade, Collection, Entity, Enum, Index, ManyToOne, OneToMany, Property, Unique } from '@mikro-orm/core';
import { BaseEntity } from '../../../common/entities/base.entity';
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
import { PaymentGatewayEnum, PaymentMethodEnum } from '../interface/payment';
import { PaymentMethodCard } from './payment-method-card.entity';
@Entity({ tableName: 'payment_methods' })
@Unique({ properties: ['restaurant', 'method'] })
@@ -20,23 +21,11 @@ export class PaymentMethod extends BaseEntity {
@Property({ nullable: true })
description?: string;
@Property({ nullable: true })
cardNumber?: string;
@Property({ nullable: true })
cardNumber2?: string;
@Property({ nullable: true })
cardNumber3?: string;
@Property({ nullable: true })
cardOwner?: string;
@Property({ nullable: true })
cardOwner2?: string;
@Property({ nullable: true })
cardOwner3?: string;
@OneToMany(() => PaymentMethodCard, card => card.paymentMethod, {
cascade: [Cascade.ALL],
orphanRemoval: true,
})
cards = new Collection<PaymentMethodCard>(this);
@Property({ default: true })
enabled: boolean = true;
+2 -1
View File
@@ -2,6 +2,7 @@ import { Module, forwardRef } from '@nestjs/common';
import { PaymentsService } from './services/payments.service';
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { PaymentMethod } from './entities/payment-method.entity';
import { PaymentMethodCard } from './entities/payment-method-card.entity';
import { PaymentMethodRepository } from './repositories/payment-method.repository';
import { PaymentMethodService } from './services/payment-method.service';
import { Restaurant } from '../restaurants/entities/restaurant.entity';
@@ -20,7 +21,7 @@ import { OrdersModule } from '../orders/orders.module';
import { RestaurantsModule } from '../restaurants/restaurants.module';
@Module({
imports: [MikroOrmModule.forFeature([PaymentMethod, Payment, Restaurant]),
imports: [MikroOrmModule.forFeature([PaymentMethod, PaymentMethodCard, Payment, Restaurant]),
AuthModule, JwtModule, InventoryModule, AdminModule, NotificationsModule,
RestaurantsModule,
forwardRef(() => OrdersModule)],
@@ -1,9 +1,11 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { EntityManager } from '@mikro-orm/postgresql';
import { PaymentMethod } from '../entities/payment-method.entity';
import { PaymentMethodCard } from '../entities/payment-method-card.entity';
import { PaymentMethodRepository } from '../repositories/payment-method.repository';
import { CreatePaymentMethodDto } from '../dto/create-payment-method.dto';
import { UpdatePaymentMethodDto } from '../dto/update-payment-method.dto';
import { PaymentMethodCardDto } from '../dto/payment-method-card.dto';
import { RequiredEntityData } from '@mikro-orm/core';
import { PaymentMessage } from 'src/common/enums/message.enum';
import { RestaurantsService } from 'src/modules/restaurants/providers/restaurants.service';
@@ -18,20 +20,23 @@ export class PaymentMethodService {
) { }
async create(restId: string, createPaymentMethodDto: CreatePaymentMethodDto): Promise<PaymentMethod> {
// Check if restaurant exists
const restaurant = await this.restaurantsService.findOneOrFail(restId);
this.assertCreditCardOwner(createPaymentMethodDto.method, createPaymentMethodDto.cardOwner);
const { cards, ...paymentMethodData } = createPaymentMethodDto;
this.assertCreditCards(createPaymentMethodDto.method, cards);
const paymentMethod = this.paymentMethodRepository.create({
restaurant,
...createPaymentMethodDto,
...paymentMethodData,
} as unknown as RequiredEntityData<PaymentMethod>);
this.assignCards(paymentMethod, cards);
await this.em.persistAndFlush(paymentMethod);
return paymentMethod;
}
async findOneOrFail(id: string): Promise<PaymentMethod> {
const paymentMethod = await this.paymentMethodRepository.findOne({ id }, { populate: ['restaurant'] });
const paymentMethod = await this.paymentMethodRepository.findOne({ id }, { populate: ['restaurant', 'cards'] });
if (!paymentMethod) {
throw new NotFoundException(PaymentMessage.PAYMENT_METHOD_NOT_FOUND);
}
@@ -39,19 +44,55 @@ export class PaymentMethodService {
}
async findByRestaurant(restaurantId: string): Promise<PaymentMethod[]> {
return this.paymentMethodRepository.find({ restaurant: { id: restaurantId } }, { populate: ['restaurant'] });
return this.paymentMethodRepository.find(
{ restaurant: { id: restaurantId } },
{ populate: ['restaurant', 'cards'] },
);
}
async update(id: string, updatePaymentMethodDto: UpdatePaymentMethodDto): Promise<PaymentMethod> {
const paymentMethod = await this.findOneOrFail(id);
this.em.assign(paymentMethod, updatePaymentMethodDto);
this.assertCreditCardOwner(paymentMethod.method, paymentMethod.cardOwner);
const { cards, ...paymentMethodData } = updatePaymentMethodDto;
this.em.assign(paymentMethod, paymentMethodData);
if (cards !== undefined) {
this.assignCards(paymentMethod, cards);
}
this.assertCreditCards(
paymentMethod.method,
paymentMethod.cards.getItems().map(card => ({ cardNumber: card.cardNumber, owner: card.owner })),
);
await this.em.flush();
return paymentMethod;
}
private assertCreditCardOwner(method: PaymentMethodEnum, cardOwner?: string): void {
if (method === PaymentMethodEnum.CreditCard && !cardOwner?.trim()) {
private assignCards(paymentMethod: PaymentMethod, cards?: PaymentMethodCardDto[]): void {
if (!cards) {
return;
}
paymentMethod.cards.removeAll();
for (const card of cards) {
paymentMethod.cards.add(
this.em.create(PaymentMethodCard, {
paymentMethod,
cardNumber: card.cardNumber,
owner: card.owner,
}),
);
}
}
private assertCreditCards(method: PaymentMethodEnum, cards?: PaymentMethodCardDto[]): void {
if (method !== PaymentMethodEnum.CreditCard) {
return;
}
const hasValidCard = cards?.some(card => card.cardNumber?.trim() && card.owner?.trim());
if (!hasValidCard) {
throw new BadRequestException(PaymentMessage.CARD_OWNER_REQUIRED);
}
}
@@ -61,6 +102,4 @@ export class PaymentMethodService {
await this.em.removeAndFlush(paymentMethod);
return paymentMethod;
}
}
@@ -57,7 +57,7 @@ export class PaymentsService {
const order = await this.em.findOne(
Order,
{ id: orderId },
{ populate: ['user', 'restaurant', 'paymentMethod', 'paymentMethod.restaurant'] },
{ populate: ['user', 'restaurant', 'paymentMethod', 'paymentMethod.restaurant', 'paymentMethod.cards'] },
);
if (!order) {
@@ -79,13 +79,6 @@ export class PaymentsService {
if (!pm.restaurant?.domain) throw new BadRequestException(PaymentMessage.RESTAURANT_DOMAIN_REQUIRED);
}
if (pm.method === PaymentMethodEnum.CreditCard && !pm.cardNumber) {
throw new BadRequestException(PaymentMessage.CARD_NUMBER_REQUIRED);
}
if (pm.method === PaymentMethodEnum.CreditCard && !pm.cardOwner?.trim()) {
throw new BadRequestException(PaymentMessage.CARD_OWNER_REQUIRED);
}
return {
order,