Cấu hình & Credential
1. Tổng quan
Dịch vụ Payment quản lý cấu hình nhà cung cấp thanh toán đã mã hóa và credential theo merchant lưu trong bảng
Configuration(schemapublic). Cấu hình được tải khi khởi động để khởi tạo MQ-Pay, trong khi credential được truy xuất theo yêu cầu mỗi request thanh toán. Cả hai đều được mã hóa bằng AES-256-GCM sử dụngAPP_ENV_APPLICATION_SECRETvà giải mã tại tầng service — dữ liệu mã hóa thô không bao giờ bị lộ.
2. PaymentConfigurationService
Nguồn: src/services/payment-configuration.service.tsKế thừa: BaseServicePhụ thuộc DI: ConfigurationRepository (qua @inject) Nội bộ: CryptoUtility.getInstance() — singleton, khởi tạo trong constructor
2.1. Constructor
constructor(
@inject({
key: BindingKeys.build({
namespace: BindingNamespaces.REPOSITORY,
key: ConfigurationRepository.name,
}),
})
private readonly _configurationRepository: ConfigurationRepository,
) {
super({ scope: PaymentConfigurationService.name });
this._crypto = CryptoUtility.getInstance();
}2.2. getPaymentConfiguration()
Chữ ký:
async getPaymentConfiguration<ReturnType = AnyObject>(opts: {
code: typeof SystemConfigurations.VNPAY_QR_MMS
| typeof SystemConfigurations.VNPAY_PHONE_POS
| string;
}): Promise<ReturnType>Mục đích: Tải cấu hình nhà cung cấp (VD: cài đặt kết nối VNPAY QR MMS) từ bảng Configuration, giải mã tValue, và trả về JSON đã parse.
Luồng:
Tham số truy vấn:
| Bộ lọc | Giá trị | Nguồn |
|---|---|---|
group | 200_INTEGRATION | ConfigurationGroups.INTEGRATION |
status | ACTIVATED | ConfigurationStatuses.ACTIVATED |
code | VD: VNPAY_QR_MMS | opts.code |
environment | DEVELOPMENT hoặc PRODUCTION | Tự phát hiện qua applicationEnvironment.isDevelopment() |
Xử lý lỗi:
| Điều kiện | Mã Trạng thái | Thông báo |
|---|---|---|
Config không tìm thấy hoặc không có tValue | 404 NotFound | Payment configuration not found | code: {code} |
| Giải mã thất bại | 500 InternalServerError | Failed to decrypt payment configuration | code: {code} |
Được gọi lúc khởi động trong ApplicationPaymentComponent.setupMQPay() cho:
SystemConfigurations.VNPAY_QR_MMS→IMQPayOptions.vnpayQrMMSSystemConfigurations.VNPAY_PHONE_POS→IMQPayOptions.vnpayPhonePos
Cả hai cuộc gọi được thực hiện song song qua Promise.all().
2.3. getPaymentCredential()
Chữ ký:
async getPaymentCredential(opts: {
provider: TMQPayProvider;
action: TMQPayCredentialAction;
types: TMQPayCredentialType[];
context: IMQPayCredentialContext;
}): Promise<IMQPayCredentialResult[]>Mục đích: Truy vấn credential thanh toán theo merchant từ bảng Configuration, bypass bộ lọc hidden property của repository để truy cập cột credential đã mã hóa trực tiếp qua Drizzle ORM.
Luồng:
IMPORTANT
Phương thức này bypass API repository tiêu chuẩn bằng cách sử dụng this._configurationRepository.getConnector() để lấy Drizzle query builder trực tiếp. Điều này cần thiết vì credential là thuộc tính ẩn trong model Configuration (hiddenProperties: ['credential', 'createdAt', 'modifiedAt', 'deletedAt']), nghĩa là .find() và .findOne() loại trừ nó khỏi kết quả.
Truy vấn Drizzle raw:
const connector = this._configurationRepository.getConnector();
const typesArray = sql.raw(`ARRAY[${types.map(t => `'${t}'`).join(', ')}]`);
const configs = await connector
.select({
id: ConfigurationSchema.id,
metadata: ConfigurationSchema.metadata,
credential: ConfigurationSchema.credential,
})
.from(ConfigurationSchema)
.where(
and(
eq(ConfigurationSchema.group, ConfigurationGroups.INTEGRATION),
eq(ConfigurationSchema.status, ConfigurationStatuses.ACTIVATED),
eq(ConfigurationSchema.principalType, Merchant.name), // 'Merchant'
eq(ConfigurationSchema.principalId, context.merchant.id),
eq(ConfigurationSchema.environment, environment),
isNull(ConfigurationSchema.deletedAt), // Kiểm tra soft-delete tường minh
sql`${ConfigurationSchema.metadata}->>'type' = ${ConfigurationMetadataTypes.PAYMENT_PROVIDER}`,
sql`${ConfigurationSchema.metadata}->>'provider' = ${provider}`,
sql`${ConfigurationSchema.metadata}->'credential'->>'action' = ${action}`,
sql`${ConfigurationSchema.metadata}->'credential'->>'type' = ANY(${typesArray})`,
),
);Chi tiết bộ lọc truy vấn:
| Bộ lọc | Cột/Biểu thức | Giá trị | Mục đích |
|---|---|---|---|
| Group | group | 200_INTEGRATION | Chỉ config tích hợp |
| Status | status | ACTIVATED | Chỉ config active |
| Principal type | principalType | Merchant | Credential theo merchant |
| Principal ID | principalId | context.merchant.id | Merchant cụ thể |
| Environment | environment | DEVELOPMENT / PRODUCTION | Theo môi trường |
| Soft delete | deletedAt IS NULL | — | Kiểm tra tường minh (bypass SoftDeletableRepository) |
| Metadata type | metadata->>'type' | PAYMENT_PROVIDER | Discriminator |
| Provider | metadata->>'provider' | VD: VNPAY_QR_MMS | Lọc nhà cung cấp |
| Credential action | metadata->'credential'->>'action' | VD: CREATE_PAYMENT | Lọc hành động |
| Credential type | metadata->'credential'->>'type' | ANY(ARRAY['request', 'ipn']) | Lọc loại (multi-match) |
Kiểu trả về:
interface IMQPayCredentialResult<CredentialType = TNullable<SecretKeyType>> {
provider: TMQPayProvider; // VD: 'VNPAY_QR_MMS'
action: TMQPayCredentialAction; // VD: 'CREATE_PAYMENT'
type: TMQPayCredentialType; // VD: 'request'
credential: CredentialType; // Chuỗi credential đã giải mã (nullable)
}Xử lý lỗi:
- Thiếu merchant ID → throw error chung
- Giải mã thất bại → ghi log lỗi cho mỗi credential, push kết quả với credential
null(không throw) - Không tìm thấy → ghi log cảnh báo, trả về mảng rỗng
[]
2.4. Credential Getter Binding
Trong ApplicationPaymentComponent.setupMQPay(), service được bọc thành callback credential getter:
this.application.bind<IMQPayOptions>({ key: MQPayBindingKeys.MQ_PAY_CLIENT_OPTIONS }).toValue({
credentialGetter: opts => paymentConfigService.getPaymentCredential(opts),
// ... các options khác
});MQ-Pay gọi getter này lúc runtime khi cần credential cho một hành động nhà cung cấp cụ thể (VD: tạo thanh toán VNPAY QR).
3. Mã hóa AES-256-GCM
3.1. CryptoUtility
Nguồn: packages/core/src/utilities/crypto.utility.tsMẫu: Singleton (CryptoUtility.getInstance()) Nguồn khóa: Biến môi trường APP_ENV_APPLICATION_SECRET
export class CryptoUtility {
private static _instance: CryptoUtility;
private readonly _aes: AES;
private readonly _encryptionKey: string;
private constructor() {
this._aes = AES.withAlgorithm('aes-256-gcm');
this._encryptionKey = applicationEnvironment.get<string>(
EnvironmentKeys.APP_ENV_APPLICATION_SECRET,
);
}
encrypt(text: string): string {
return this._aes.encrypt({ message: text, secret: this._encryptionKey });
}
decrypt(encryptedText: string): string {
return this._aes.decrypt({ message: encryptedText, secret: this._encryptionKey });
}
sign(opts: ISignOptions): string {
const { timestamp, eventType, parts, secret } = opts;
const rawPayload = [timestamp, eventType, ...parts].join('|');
return hash(rawPayload, { algorithm: 'SHA256', outputType: 'base64', secret });
}
}3.2. Chi tiết Mã hóa
| Thuộc tính | Giá trị |
|---|---|
| Thuật toán | AES-256-GCM (mã hóa xác thực) |
| Nguồn khóa | APP_ENV_APPLICATION_SECRET |
| Dẫn xuất khóa | Sử dụng trực tiếp bởi IGNIS AES.withAlgorithm('aes-256-gcm') |
| Trường được mã hóa | Configuration.tValue (cấu hình nhà cung cấp), Configuration.credential (secret keys nhà cung cấp) |
| Phương thức bổ sung | sign() — HMAC SHA-256 cho xác minh chữ ký webhook |
3.3. Trường nào Được Mã hóa
| Entity | Trường | Chứa | Khi nào Giải mã |
|---|---|---|---|
Configuration | tValue | Cài đặt kết nối nhà cung cấp (chuỗi JSON) | Lúc khởi động qua getPaymentConfiguration() |
Configuration | credential | Secret keys / tokens nhà cung cấp | Theo yêu cầu qua getPaymentCredential() |
WARNING
APP_ENV_APPLICATION_SECRET phải giống nhau trên tất cả dịch vụ đọc cấu hình thanh toán (Payment, Sale, v.v.). Không khớp sẽ khiến AES.decrypt() thất bại với lỗi giải mã. Không có cơ chế xoay khóa — thay đổi secret yêu cầu mã hóa lại toàn bộ dữ liệu đã lưu.
4. Trình tự Khởi tạo ApplicationPaymentComponent
Trình tự khởi tạo đầy đủ khi RUN_MODE=startup:
| Giai đoạn | Bước | Hành động | DI Key |
|---|---|---|---|
| Repositories | 1 | Đăng ký ConfigurationRepository | repositories.ConfigurationRepository |
| 2 | Đăng ký WebhookConfigRepository | repositories.WebhookConfigRepository | |
| 3 | Đăng ký MigrationRepository | repositories.MigrationRepository | |
| Services | 4 | Đăng ký PaymentConfigurationService | services.PaymentConfigurationService |
| 5 | Đăng ký WebhookDispatcherService | services.WebhookDispatcherService | |
| Controllers | 6 | Đăng ký WebhookConfigController | — |
| MQ-Pay | 7 | Resolve PaymentConfigurationService từ DI | — |
| 8 | Resolve WebhookConfigRepository từ DI | — | |
| 9 | Resolve WebhookDispatcherService từ DI | — | |
| 10 | Tạo WebhookEventHandlerHelper (new thủ công) | — | |
| 11 | Promise.all(): Tải config VNPAY_QR_MMS + VNPAY_PHONE_POS | — | |
| 12 | Bind IMQPayOptions vào DI | @nx-3rd/mq-pay/client-options | |
| 13 | Mount MQPayComponent | — |
NOTE
Khi RUN_MODE không phải startup (VD: RUN_MODE=migrate), bước 7–13 bị bỏ qua hoàn toàn. Chỉ repositories, services và controllers được đăng ký. Điều này đảm bảo migrations có thể chạy mà không cần hạ tầng MQ-Pay (Redis, BullMQ, VNPAY credentials).
5. Dữ liệu Khởi tạo
Nguồn: src/migrations/processes/migration-process.ts
export const getMigrationProcesses = createMigrationProcessLoader({
seedPaths: [
'payment-0001-seed-vnpay-qr-mms-configuration',
'payment-0002-seed-vnpay-phone-pos-configuration',
],
importFn: path => import(`../processes/${path}.js`),
});5.1. Seed VNPAY QR MMS
File: payment-0001-seed-vnpay-qr-mms-configuration
| Cài đặt | Giá trị | Ghi chú |
|---|---|---|
| Nhà cung cấp | VNPAY_QR_MMS | Thanh toán mã QR động |
| Mã | VNPAY_QR_MMS | Khớp SystemConfigurations.VNPAY_QR_MMS |
| Nhóm | 200_INTEGRATION | ConfigurationGroups.INTEGRATION |
| Trạng thái | ACTIVATED | Active ngay lập tức |
| Mặc định | true | Nhà cung cấp thanh toán mặc định |
| Production | false | Chỉ môi trường development |
| App ID | MERCHANT | Định danh ứng dụng VNPAY |
| Master Merchant Code | A000000775 | Master merchant VNPAY |
5.2. Seed VNPAY Phone POS
File: payment-0002-seed-vnpay-phone-pos-configuration
| Cài đặt | Giá trị | Ghi chú |
|---|---|---|
| Nhà cung cấp | VNPAY_PHONE_POS | Thanh toán thẻ NFC |
| Mã | VNPAY_PHONE_POS | Khớp SystemConfigurations.VNPAY_PHONE_POS |
| Nhóm | 200_INTEGRATION | ConfigurationGroups.INTEGRATION |
| Trạng thái | ACTIVATED | Active ngay lập tức |
| Mặc định | false | Không phải nhà cung cấp mặc định |
| Production | false | Chỉ môi trường development |
6. Tham chiếu Model Configuration
6.1. Định nghĩa Entity
Nguồn: packages/core/src/models/schemas/public/configuration/model.ts
@model({
type: 'entity',
settings: {
hiddenProperties: ['credential', 'createdAt', 'modifiedAt', 'deletedAt'],
defaultFilter: { where: { deletedAt: null } },
},
})
export class Configuration extends BaseEntity<typeof ConfigurationSchema> {
static override TABLE_NAME = 'Configuration';
static override schema = ConfigurationSchema;
}Cài đặt model chính:
hiddenProperties: Các trường này bị loại trừ khỏi tất cả kết quả.find()/.findOne()của repositorydefaultFilter: Tự động thêmWHERE deletedAt IS NULLvào mọi truy vấn (soft-delete)
6.2. ConfigurationMetadataTypes
Hệ thống discriminated union cho cột metadata JSONB:
| Loại | Hằng số | Interface | Mô tả |
|---|---|---|---|
PAYMENT_PROVIDER | ConfigurationMetadataTypes.PAYMENT_PROVIDER | IPaymentProviderMetadata | Credential nhà cung cấp thanh toán |
interface IPaymentProviderMetadata {
type: 'PAYMENT_PROVIDER';
provider: string; // TMQPayProvider (VD: 'VNPAY_QR_MMS')
credential: {
action: string; // TMQPayCredentialAction (VD: 'CREATE_PAYMENT')
type: string; // TMQPayCredentialType (VD: 'request')
};
}6.3. ConfigurationGroups
| Nhóm | Hằng số | Sử dụng |
|---|---|---|
000_SYSTEM | ConfigurationGroups.SYSTEM | Cài đặt cấp hệ thống |
100_TABLE | ConfigurationGroups.TABLE | Cấu hình bảng UI |
200_INTEGRATION | ConfigurationGroups.INTEGRATION | Nhà cung cấp thanh toán, tích hợp bên ngoài |
6.4. ConfigurationStatuses
| Trạng thái | Hằng số | Mô tả |
|---|---|---|
ACTIVATED | ConfigurationStatuses.ACTIVATED | Active — được sử dụng trong truy vấn |
DEACTIVATED | ConfigurationStatuses.DEACTIVATED | Tạm vô hiệu |
ARCHIVED | ConfigurationStatuses.ARCHIVED | Vô hiệu vĩnh viễn |
7. Tài liệu Liên quan
| Tài liệu | Mô tả |
|---|---|
| Tổng quan & Thiết lập | Kiến trúc, mô hình dữ liệu, components, services |
| Điều phối Webhook | Xử lý sự kiện, cơ chế thử lại, định dạng payload |
| Triển khai | Định tuyến Traefik, Docker, health check |
| Gói Core | Schema Configuration, CryptoUtility |