add : PaymentCode And Customer Card

This commit is contained in:
2026-03-30 16:34:19 +03:00
parent 42de277a1e
commit 68529a182f
3 changed files with 376 additions and 24 deletions

View File

@@ -1,5 +1,5 @@
import { ApiPropertyOptional } from '@nestjs/swagger'; import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsBoolean, IsInt, IsObject, IsOptional, IsString, MaxLength, Min } from 'class-validator'; import { IsBoolean, IsInt, IsOptional, IsString, MaxLength, Min } from 'class-validator';
export class UpdatePaymentMethodSettingDto { export class UpdatePaymentMethodSettingDto {
@ApiPropertyOptional() @ApiPropertyOptional()
@@ -36,10 +36,127 @@ export class UpdatePaymentMethodSettingDto {
instructions?: string | null; instructions?: string | null;
@ApiPropertyOptional({ @ApiPropertyOptional({
description: 'Gateway-specific config. Example: terminalId, merchantId, callbackUrl.', description: 'آدرس callback یا return URL درگاه آنلاین.',
type: Object, example: 'https://admin.example.com/payment/callback/zarinpal',
}) })
@IsOptional() @IsOptional()
@IsObject() @IsString()
config?: Record<string, unknown>; @MaxLength(500)
callbackUrl?: string | null;
@ApiPropertyOptional({
description: 'کد مرچنت زرین‌پال',
example: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
})
@IsOptional()
@IsString()
@MaxLength(120)
zarinpalMerchantId?: string | null;
@ApiPropertyOptional({
description: 'شماره ترمینال سامان',
example: '1234567',
})
@IsOptional()
@IsString()
@MaxLength(120)
samanTerminalId?: string | null;
@ApiPropertyOptional({
description: 'شماره ترمینال به‌پرداخت ملت',
example: '1234567',
})
@IsOptional()
@IsString()
@MaxLength(120)
mellatTerminalId?: string | null;
@ApiPropertyOptional({
description: 'نام کاربری سرویس به‌پرداخت ملت',
example: 'bp_user',
})
@IsOptional()
@IsString()
@MaxLength(120)
mellatUsername?: string | null;
@ApiPropertyOptional({
description: 'رمز عبور سرویس به‌پرداخت ملت',
example: 'secret-password',
})
@IsOptional()
@IsString()
@MaxLength(255)
mellatPassword?: string | null;
@ApiPropertyOptional({
description: 'کد پذیرنده پاسارگاد',
example: '12345678',
})
@IsOptional()
@IsString()
@MaxLength(120)
pasargadMerchantCode?: string | null;
@ApiPropertyOptional({
description: 'کد ترمینال پاسارگاد',
example: '1234567',
})
@IsOptional()
@IsString()
@MaxLength(120)
pasargadTerminalCode?: string | null;
@ApiPropertyOptional({
description: 'محتوای PEM یا certificate متنی پاسارگاد',
example: '-----BEGIN CERTIFICATE----- ...',
})
@IsOptional()
@IsString()
pasargadCertificatePem?: string | null;
@ApiPropertyOptional({
description: 'نام بانک برای نمایش در ثبت فیش بانکی',
example: 'بانک ملت',
})
@IsOptional()
@IsString()
@MaxLength(120)
bankName?: string | null;
@ApiPropertyOptional({
description: 'نام صاحب حساب برای فیش بانکی',
example: 'شرکت پارس شاپ',
})
@IsOptional()
@IsString()
@MaxLength(160)
accountHolderName?: string | null;
@ApiPropertyOptional({
description: 'شماره حساب برای فیش بانکی',
example: '1234567890',
})
@IsOptional()
@IsString()
@MaxLength(80)
accountNumber?: string | null;
@ApiPropertyOptional({
description: 'شماره کارت برای فیش بانکی',
example: '6037991234567890',
})
@IsOptional()
@IsString()
@MaxLength(30)
cardNumber?: string | null;
@ApiPropertyOptional({
description: 'شماره شبا برای فیش بانکی',
example: 'IR820540102680020817909002',
})
@IsOptional()
@IsString()
@MaxLength(40)
shebaNumber?: string | null;
} }

View File

@@ -44,8 +44,47 @@ export class PaymentMethodSetting {
@Column({ type: 'text', nullable: true }) @Column({ type: 'text', nullable: true })
instructions?: string | null; instructions?: string | null;
@Column({ type: 'jsonb', default: {} }) @Column({ name: 'callback_url', type: 'varchar', length: 500, nullable: true })
config: Record<string, unknown>; callbackUrl?: string | null;
@Column({ name: 'zarinpal_merchant_id', type: 'varchar', length: 120, nullable: true })
zarinpalMerchantId?: string | null;
@Column({ name: 'saman_terminal_id', type: 'varchar', length: 120, nullable: true })
samanTerminalId?: string | null;
@Column({ name: 'mellat_terminal_id', type: 'varchar', length: 120, nullable: true })
mellatTerminalId?: string | null;
@Column({ name: 'mellat_username', type: 'varchar', length: 120, nullable: true })
mellatUsername?: string | null;
@Column({ name: 'mellat_password', type: 'varchar', length: 255, nullable: true })
mellatPassword?: string | null;
@Column({ name: 'pasargad_merchant_code', type: 'varchar', length: 120, nullable: true })
pasargadMerchantCode?: string | null;
@Column({ name: 'pasargad_terminal_code', type: 'varchar', length: 120, nullable: true })
pasargadTerminalCode?: string | null;
@Column({ name: 'pasargad_certificate_pem', type: 'text', nullable: true })
pasargadCertificatePem?: string | null;
@Column({ name: 'bank_name', type: 'varchar', length: 120, nullable: true })
bankName?: string | null;
@Column({ name: 'account_holder_name', type: 'varchar', length: 160, nullable: true })
accountHolderName?: string | null;
@Column({ name: 'account_number', type: 'varchar', length: 80, nullable: true })
accountNumber?: string | null;
@Column({ name: 'card_number', type: 'varchar', length: 30, nullable: true })
cardNumber?: string | null;
@Column({ name: 'sheba_number', type: 'varchar', length: 40, nullable: true })
shebaNumber?: string | null;
@CreateDateColumn({ name: 'created_at' }) @CreateDateColumn({ name: 'created_at' })
createdAt: Date; createdAt: Date;

View File

@@ -11,13 +11,13 @@ import { UserOrder } from '../users/entities/user-order.entity';
import { OrderStatus } from '../users/enums/order-status.enum'; import { OrderStatus } from '../users/enums/order-status.enum';
import { PaymentStatus } from '../users/enums/payment-status.enum'; import { PaymentStatus } from '../users/enums/payment-status.enum';
import { UserRole } from '../users/enums/user-role.enum'; import { UserRole } from '../users/enums/user-role.enum';
import { PaymentMethodSetting } from './entities/payment-method-setting.entity';
import { PaymentMethodCode } from './enums/payment-method-code.enum';
import { PaymentMethodType } from './enums/payment-method-type.enum';
import { ReviewBankSlipDto } from './dto/review-bank-slip.dto'; import { ReviewBankSlipDto } from './dto/review-bank-slip.dto';
import { SelectOrderPaymentMethodDto } from './dto/select-order-payment-method.dto'; import { SelectOrderPaymentMethodDto } from './dto/select-order-payment-method.dto';
import { SubmitBankSlipDto } from './dto/submit-bank-slip.dto'; import { SubmitBankSlipDto } from './dto/submit-bank-slip.dto';
import { UpdatePaymentMethodSettingDto } from './dto/update-payment-method-setting.dto'; import { UpdatePaymentMethodSettingDto } from './dto/update-payment-method-setting.dto';
import { PaymentMethodSetting } from './entities/payment-method-setting.entity';
import { PaymentMethodCode } from './enums/payment-method-code.enum';
import { PaymentMethodType } from './enums/payment-method-type.enum';
@Injectable() @Injectable()
export class PaymentsService { export class PaymentsService {
@@ -32,7 +32,20 @@ export class PaymentsService {
| 'displayOrder' | 'displayOrder'
| 'description' | 'description'
| 'instructions' | 'instructions'
| 'config' | 'callbackUrl'
| 'zarinpalMerchantId'
| 'samanTerminalId'
| 'mellatTerminalId'
| 'mellatUsername'
| 'mellatPassword'
| 'pasargadMerchantCode'
| 'pasargadTerminalCode'
| 'pasargadCertificatePem'
| 'bankName'
| 'accountHolderName'
| 'accountNumber'
| 'cardNumber'
| 'shebaNumber'
> >
> = [ > = [
{ {
@@ -44,7 +57,20 @@ export class PaymentsService {
displayOrder: 1, displayOrder: 1,
description: 'پرداخت آنلاین از طریق زرین‌پال', description: 'پرداخت آنلاین از طریق زرین‌پال',
instructions: null, instructions: null,
config: {}, callbackUrl: null,
zarinpalMerchantId: null,
samanTerminalId: null,
mellatTerminalId: null,
mellatUsername: null,
mellatPassword: null,
pasargadMerchantCode: null,
pasargadTerminalCode: null,
pasargadCertificatePem: null,
bankName: null,
accountHolderName: null,
accountNumber: null,
cardNumber: null,
shebaNumber: null,
}, },
{ {
code: PaymentMethodCode.SAMAN, code: PaymentMethodCode.SAMAN,
@@ -55,7 +81,20 @@ export class PaymentsService {
displayOrder: 2, displayOrder: 2,
description: 'پرداخت آنلاین از طریق بانک سامان', description: 'پرداخت آنلاین از طریق بانک سامان',
instructions: null, instructions: null,
config: {}, callbackUrl: null,
zarinpalMerchantId: null,
samanTerminalId: null,
mellatTerminalId: null,
mellatUsername: null,
mellatPassword: null,
pasargadMerchantCode: null,
pasargadTerminalCode: null,
pasargadCertificatePem: null,
bankName: null,
accountHolderName: null,
accountNumber: null,
cardNumber: null,
shebaNumber: null,
}, },
{ {
code: PaymentMethodCode.MELLAT, code: PaymentMethodCode.MELLAT,
@@ -66,7 +105,20 @@ export class PaymentsService {
displayOrder: 3, displayOrder: 3,
description: 'پرداخت آنلاین از طریق بانک ملت', description: 'پرداخت آنلاین از طریق بانک ملت',
instructions: null, instructions: null,
config: {}, callbackUrl: null,
zarinpalMerchantId: null,
samanTerminalId: null,
mellatTerminalId: null,
mellatUsername: null,
mellatPassword: null,
pasargadMerchantCode: null,
pasargadTerminalCode: null,
pasargadCertificatePem: null,
bankName: null,
accountHolderName: null,
accountNumber: null,
cardNumber: null,
shebaNumber: null,
}, },
{ {
code: PaymentMethodCode.PASARGAD, code: PaymentMethodCode.PASARGAD,
@@ -77,7 +129,20 @@ export class PaymentsService {
displayOrder: 4, displayOrder: 4,
description: 'پرداخت آنلاین از طریق بانک پاسارگاد', description: 'پرداخت آنلاین از طریق بانک پاسارگاد',
instructions: null, instructions: null,
config: {}, callbackUrl: null,
zarinpalMerchantId: null,
samanTerminalId: null,
mellatTerminalId: null,
mellatUsername: null,
mellatPassword: null,
pasargadMerchantCode: null,
pasargadTerminalCode: null,
pasargadCertificatePem: null,
bankName: null,
accountHolderName: null,
accountNumber: null,
cardNumber: null,
shebaNumber: null,
}, },
{ {
code: PaymentMethodCode.BANK_SLIP, code: PaymentMethodCode.BANK_SLIP,
@@ -89,7 +154,20 @@ export class PaymentsService {
description: 'ثبت فیش بانکی و بررسی توسط ادمین', description: 'ثبت فیش بانکی و بررسی توسط ادمین',
instructions: instructions:
'پس از واریز، تصویر فیش و شماره پیگیری را ثبت کنید. سفارش در وضعیت منتظر تایید پرداخت قرار می‌گیرد.', 'پس از واریز، تصویر فیش و شماره پیگیری را ثبت کنید. سفارش در وضعیت منتظر تایید پرداخت قرار می‌گیرد.',
config: {}, callbackUrl: null,
zarinpalMerchantId: null,
samanTerminalId: null,
mellatTerminalId: null,
mellatUsername: null,
mellatPassword: null,
pasargadMerchantCode: null,
pasargadTerminalCode: null,
pasargadCertificatePem: null,
bankName: null,
accountHolderName: null,
accountNumber: null,
cardNumber: null,
shebaNumber: null,
}, },
{ {
code: PaymentMethodCode.CASH_ON_DELIVERY, code: PaymentMethodCode.CASH_ON_DELIVERY,
@@ -100,7 +178,20 @@ export class PaymentsService {
displayOrder: 6, displayOrder: 6,
description: 'پرداخت هنگام تحویل کالا', description: 'پرداخت هنگام تحویل کالا',
instructions: 'سفارش ثبت می‌شود و پرداخت هنگام تحویل انجام خواهد شد.', instructions: 'سفارش ثبت می‌شود و پرداخت هنگام تحویل انجام خواهد شد.',
config: {}, callbackUrl: null,
zarinpalMerchantId: null,
samanTerminalId: null,
mellatTerminalId: null,
mellatUsername: null,
mellatPassword: null,
pasargadMerchantCode: null,
pasargadTerminalCode: null,
pasargadCertificatePem: null,
bankName: null,
accountHolderName: null,
accountNumber: null,
cardNumber: null,
shebaNumber: null,
}, },
]; ];
@@ -133,11 +224,43 @@ export class PaymentsService {
...(dto.title !== undefined ? { title: dto.title } : {}), ...(dto.title !== undefined ? { title: dto.title } : {}),
...(dto.description !== undefined ? { description: dto.description } : {}), ...(dto.description !== undefined ? { description: dto.description } : {}),
...(dto.instructions !== undefined ? { instructions: dto.instructions } : {}), ...(dto.instructions !== undefined ? { instructions: dto.instructions } : {}),
...(dto.config !== undefined ...(dto.callbackUrl !== undefined ? { callbackUrl: dto.callbackUrl } : {}),
? { config: { ...(method.config ?? {}), ...dto.config } } ...(dto.zarinpalMerchantId !== undefined
? { zarinpalMerchantId: dto.zarinpalMerchantId }
: {}), : {}),
...(dto.samanTerminalId !== undefined
? { samanTerminalId: dto.samanTerminalId }
: {}),
...(dto.mellatTerminalId !== undefined
? { mellatTerminalId: dto.mellatTerminalId }
: {}),
...(dto.mellatUsername !== undefined
? { mellatUsername: dto.mellatUsername }
: {}),
...(dto.mellatPassword !== undefined
? { mellatPassword: dto.mellatPassword }
: {}),
...(dto.pasargadMerchantCode !== undefined
? { pasargadMerchantCode: dto.pasargadMerchantCode }
: {}),
...(dto.pasargadTerminalCode !== undefined
? { pasargadTerminalCode: dto.pasargadTerminalCode }
: {}),
...(dto.pasargadCertificatePem !== undefined
? { pasargadCertificatePem: dto.pasargadCertificatePem }
: {}),
...(dto.bankName !== undefined ? { bankName: dto.bankName } : {}),
...(dto.accountHolderName !== undefined
? { accountHolderName: dto.accountHolderName }
: {}),
...(dto.accountNumber !== undefined
? { accountNumber: dto.accountNumber }
: {}),
...(dto.cardNumber !== undefined ? { cardNumber: dto.cardNumber } : {}),
...(dto.shebaNumber !== undefined ? { shebaNumber: dto.shebaNumber } : {}),
}); });
this.validateMethodCredentials(method);
return this.paymentMethodSettingsRepository.save(method); return this.paymentMethodSettingsRepository.save(method);
} }
@@ -185,6 +308,7 @@ export class PaymentsService {
paymentUrl: this.buildMockGatewayUrl(savedOrder, method), paymentUrl: this.buildMockGatewayUrl(savedOrder, method),
sandboxEnabled: method.isSandboxEnabled, sandboxEnabled: method.isSandboxEnabled,
gateway: method.code, gateway: method.code,
callbackUrl: method.callbackUrl ?? null,
}, },
}; };
} }
@@ -203,6 +327,7 @@ export class PaymentsService {
action: { action: {
type: 'upload_bank_slip', type: 'upload_bank_slip',
instructions: method.instructions, instructions: method.instructions,
bankAccount: this.serializeBankSlipInfo(method),
}, },
}; };
} }
@@ -241,7 +366,10 @@ export class PaymentsService {
} }
if (file) { if (file) {
const upload = await this.storageService.uploadPublicFile(file, 'payments/bank-slips'); const upload = await this.storageService.uploadPublicFile(
file,
'payments/bank-slips',
);
if (order.bankSlipImageUrl && order.bankSlipImageUrl !== upload.url) { if (order.bankSlipImageUrl && order.bankSlipImageUrl !== upload.url) {
await this.storageService.deletePublicFileByUrl(order.bankSlipImageUrl); await this.storageService.deletePublicFileByUrl(order.bankSlipImageUrl);
} }
@@ -418,9 +546,80 @@ export class PaymentsService {
instructions: method.instructions, instructions: method.instructions,
isSandboxEnabled: method.isSandboxEnabled, isSandboxEnabled: method.isSandboxEnabled,
displayOrder: method.displayOrder, displayOrder: method.displayOrder,
...(method.type === PaymentMethodType.BANK_SLIP
? {
bankAccount: this.serializeBankSlipInfo(method),
}
: {}),
}; };
} }
private serializeBankSlipInfo(method: PaymentMethodSetting) {
return {
bankName: method.bankName ?? null,
accountHolderName: method.accountHolderName ?? null,
accountNumber: method.accountNumber ?? null,
cardNumber: method.cardNumber ?? null,
shebaNumber: method.shebaNumber ?? null,
};
}
private validateMethodCredentials(method: PaymentMethodSetting) {
if (!method.isEnabled) {
return;
}
switch (method.code) {
case PaymentMethodCode.ZARINPAL:
this.ensureRequiredFields(method, ['zarinpalMerchantId', 'callbackUrl']);
break;
case PaymentMethodCode.SAMAN:
this.ensureRequiredFields(method, ['samanTerminalId', 'callbackUrl']);
break;
case PaymentMethodCode.MELLAT:
this.ensureRequiredFields(method, [
'mellatTerminalId',
'mellatUsername',
'mellatPassword',
'callbackUrl',
]);
break;
case PaymentMethodCode.PASARGAD:
this.ensureRequiredFields(method, [
'pasargadMerchantCode',
'pasargadTerminalCode',
'pasargadCertificatePem',
'callbackUrl',
]);
break;
case PaymentMethodCode.BANK_SLIP:
this.ensureRequiredFields(method, [
'bankName',
'accountHolderName',
'accountNumber',
]);
break;
default:
break;
}
}
private ensureRequiredFields(
method: PaymentMethodSetting,
fields: Array<keyof PaymentMethodSetting>,
) {
const missingFields = fields.filter((field) => {
const value = method[field];
return value === null || value === undefined || value === '';
});
if (missingFields.length > 0) {
throw new BadRequestException(
`Missing required fields for ${method.code}: ${missingFields.join(', ')}`,
);
}
}
private mapOrderPayment(order: UserOrder) { private mapOrderPayment(order: UserOrder) {
return { return {
id: order.id, id: order.id,
@@ -441,10 +640,7 @@ export class PaymentsService {
}; };
} }
private buildMockGatewayUrl( private buildMockGatewayUrl(order: UserOrder, method: PaymentMethodSetting) {
order: UserOrder,
method: PaymentMethodSetting,
) {
const mode = method.isSandboxEnabled ? 'sandbox' : 'production'; const mode = method.isSandboxEnabled ? 'sandbox' : 'production';
return `https://payments.mock.local/${method.code}/${mode}/pay?orderId=${order.id}`; return `https://payments.mock.local/${method.code}/${mode}/pay?orderId=${order.id}`;
} }