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 { IsBoolean, IsInt, IsObject, IsOptional, IsString, MaxLength, Min } from 'class-validator';
import { IsBoolean, IsInt, IsOptional, IsString, MaxLength, Min } from 'class-validator';
export class UpdatePaymentMethodSettingDto {
@ApiPropertyOptional()
@@ -36,10 +36,127 @@ export class UpdatePaymentMethodSettingDto {
instructions?: string | null;
@ApiPropertyOptional({
description: 'Gateway-specific config. Example: terminalId, merchantId, callbackUrl.',
type: Object,
description: 'آدرس callback یا return URL درگاه آنلاین.',
example: 'https://admin.example.com/payment/callback/zarinpal',
})
@IsOptional()
@IsObject()
config?: Record<string, unknown>;
@IsString()
@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 })
instructions?: string | null;
@Column({ type: 'jsonb', default: {} })
config: Record<string, unknown>;
@Column({ name: 'callback_url', type: 'varchar', length: 500, nullable: true })
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' })
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 { PaymentStatus } from '../users/enums/payment-status.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 { SelectOrderPaymentMethodDto } from './dto/select-order-payment-method.dto';
import { SubmitBankSlipDto } from './dto/submit-bank-slip.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()
export class PaymentsService {
@@ -32,7 +32,20 @@ export class PaymentsService {
| 'displayOrder'
| 'description'
| '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,
description: 'پرداخت آنلاین از طریق زرین‌پال',
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,
@@ -55,7 +81,20 @@ export class PaymentsService {
displayOrder: 2,
description: 'پرداخت آنلاین از طریق بانک سامان',
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,
@@ -66,7 +105,20 @@ export class PaymentsService {
displayOrder: 3,
description: 'پرداخت آنلاین از طریق بانک ملت',
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,
@@ -77,7 +129,20 @@ export class PaymentsService {
displayOrder: 4,
description: 'پرداخت آنلاین از طریق بانک پاسارگاد',
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,
@@ -89,7 +154,20 @@ export class PaymentsService {
description: 'ثبت فیش بانکی و بررسی توسط ادمین',
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,
@@ -100,7 +178,20 @@ export class PaymentsService {
displayOrder: 6,
description: 'پرداخت هنگام تحویل کالا',
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.description !== undefined ? { description: dto.description } : {}),
...(dto.instructions !== undefined ? { instructions: dto.instructions } : {}),
...(dto.config !== undefined
? { config: { ...(method.config ?? {}), ...dto.config } }
...(dto.callbackUrl !== undefined ? { callbackUrl: dto.callbackUrl } : {}),
...(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);
}
@@ -185,6 +308,7 @@ export class PaymentsService {
paymentUrl: this.buildMockGatewayUrl(savedOrder, method),
sandboxEnabled: method.isSandboxEnabled,
gateway: method.code,
callbackUrl: method.callbackUrl ?? null,
},
};
}
@@ -203,6 +327,7 @@ export class PaymentsService {
action: {
type: 'upload_bank_slip',
instructions: method.instructions,
bankAccount: this.serializeBankSlipInfo(method),
},
};
}
@@ -241,7 +366,10 @@ export class PaymentsService {
}
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) {
await this.storageService.deletePublicFileByUrl(order.bankSlipImageUrl);
}
@@ -418,9 +546,80 @@ export class PaymentsService {
instructions: method.instructions,
isSandboxEnabled: method.isSandboxEnabled,
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) {
return {
id: order.id,
@@ -441,10 +640,7 @@ export class PaymentsService {
};
}
private buildMockGatewayUrl(
order: UserOrder,
method: PaymentMethodSetting,
) {
private buildMockGatewayUrl(order: UserOrder, method: PaymentMethodSetting) {
const mode = method.isSandboxEnabled ? 'sandbox' : 'production';
return `https://payments.mock.local/${method.code}/${mode}/pay?orderId=${order.id}`;
}