Skip to content

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 (schema public). 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ụng APP_ENV_APPLICATION_SECRET và 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

typescript
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ý:

typescript
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ọcGiá trịNguồn
group200_INTEGRATIONConfigurationGroups.INTEGRATION
statusACTIVATEDConfigurationStatuses.ACTIVATED
codeVD: VNPAY_QR_MMSopts.code
environmentDEVELOPMENT hoặc PRODUCTIONTự phát hiện qua applicationEnvironment.isDevelopment()

Xử lý lỗi:

Điều kiệnMã Trạng tháiThông báo
Config không tìm thấy hoặc không có tValue404 NotFoundPayment configuration not found | code: {code}
Giải mã thất bại500 InternalServerErrorFailed to decrypt payment configuration | code: {code}

Được gọi lúc khởi động trong ApplicationPaymentComponent.setupMQPay() cho:

  • SystemConfigurations.VNPAY_QR_MMSIMQPayOptions.vnpayQrMMS
  • SystemConfigurations.VNPAY_PHONE_POSIMQPayOptions.vnpayPhonePos

Cả hai cuộc gọi được thực hiện song song qua Promise.all().

2.3. getPaymentCredential()

Chữ ký:

typescript
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ì credentialthuộc tính ẩn trong model Configuration (hiddenProperties: ['credential', 'createdAt', 'modifiedAt', 'deletedAt']), nghĩa là .find().findOne() loại trừ nó khỏi kết quả.

Truy vấn Drizzle raw:

typescript
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ọcCột/Biểu thứcGiá trịMục đích
Groupgroup200_INTEGRATIONChỉ config tích hợp
StatusstatusACTIVATEDChỉ config active
Principal typeprincipalTypeMerchantCredential theo merchant
Principal IDprincipalIdcontext.merchant.idMerchant cụ thể
EnvironmentenvironmentDEVELOPMENT / PRODUCTIONTheo môi trường
Soft deletedeletedAt IS NULLKiểm tra tường minh (bypass SoftDeletableRepository)
Metadata typemetadata->>'type'PAYMENT_PROVIDERDiscriminator
Providermetadata->>'provider'VD: VNPAY_QR_MMSLọc nhà cung cấp
Credential actionmetadata->'credential'->>'action'VD: CREATE_PAYMENTLọc hành động
Credential typemetadata->'credential'->>'type'ANY(ARRAY['request', 'ipn'])Lọc loại (multi-match)

Kiểu trả về:

typescript
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:

typescript
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

typescript
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ínhGiá trị
Thuật toánAES-256-GCM (mã hóa xác thực)
Nguồn khóaAPP_ENV_APPLICATION_SECRET
Dẫn xuất khóaSử dụng trực tiếp bởi IGNIS AES.withAlgorithm('aes-256-gcm')
Trường được mã hóaConfiguration.tValue (cấu hình nhà cung cấp), Configuration.credential (secret keys nhà cung cấp)
Phương thức bổ sungsign() — HMAC SHA-256 cho xác minh chữ ký webhook

3.3. Trường nào Được Mã hóa

EntityTrườngChứaKhi nào Giải mã
ConfigurationtValueCài đặt kết nối nhà cung cấp (chuỗi JSON)Lúc khởi động qua getPaymentConfiguration()
ConfigurationcredentialSecret keys / tokens nhà cung cấpTheo 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ạnBướcHành độngDI Key
Repositories1Đăng ký ConfigurationRepositoryrepositories.ConfigurationRepository
2Đăng ký WebhookConfigRepositoryrepositories.WebhookConfigRepository
3Đăng ký MigrationRepositoryrepositories.MigrationRepository
Services4Đăng ký PaymentConfigurationServiceservices.PaymentConfigurationService
5Đăng ký WebhookDispatcherServiceservices.WebhookDispatcherService
Controllers6Đăng ký WebhookConfigController
MQ-Pay7Resolve PaymentConfigurationService từ DI
8Resolve WebhookConfigRepository từ DI
9Resolve WebhookDispatcherService từ DI
10Tạo WebhookEventHandlerHelper (new thủ công)
11Promise.all(): Tải config VNPAY_QR_MMS + VNPAY_PHONE_POS
12Bind IMQPayOptions vào DI@nx-3rd/mq-pay/client-options
13Mount 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

typescript
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 đặtGiá trịGhi chú
Nhà cung cấpVNPAY_QR_MMSThanh toán mã QR động
VNPAY_QR_MMSKhớp SystemConfigurations.VNPAY_QR_MMS
Nhóm200_INTEGRATIONConfigurationGroups.INTEGRATION
Trạng tháiACTIVATEDActive ngay lập tức
Mặc địnhtrueNhà cung cấp thanh toán mặc định
ProductionfalseChỉ môi trường development
App IDMERCHANTĐịnh danh ứng dụng VNPAY
Master Merchant CodeA000000775Master merchant VNPAY

5.2. Seed VNPAY Phone POS

File: payment-0002-seed-vnpay-phone-pos-configuration

Cài đặtGiá trịGhi chú
Nhà cung cấpVNPAY_PHONE_POSThanh toán thẻ NFC
VNPAY_PHONE_POSKhớp SystemConfigurations.VNPAY_PHONE_POS
Nhóm200_INTEGRATIONConfigurationGroups.INTEGRATION
Trạng tháiACTIVATEDActive ngay lập tức
Mặc địnhfalseKhông phải nhà cung cấp mặc định
ProductionfalseChỉ 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

typescript
@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 repository
  • defaultFilter: Tự động thêm WHERE deletedAt IS NULL vào mọi truy vấn (soft-delete)

6.2. ConfigurationMetadataTypes

Hệ thống discriminated union cho cột metadata JSONB:

LoạiHằng sốInterfaceMô tả
PAYMENT_PROVIDERConfigurationMetadataTypes.PAYMENT_PROVIDERIPaymentProviderMetadataCredential nhà cung cấp thanh toán
typescript
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ómHằng sốSử dụng
000_SYSTEMConfigurationGroups.SYSTEMCài đặt cấp hệ thống
100_TABLEConfigurationGroups.TABLECấu hình bảng UI
200_INTEGRATIONConfigurationGroups.INTEGRATIONNhà cung cấp thanh toán, tích hợp bên ngoài

6.4. ConfigurationStatuses

Trạng tháiHằng sốMô tả
ACTIVATEDConfigurationStatuses.ACTIVATEDActive — được sử dụng trong truy vấn
DEACTIVATEDConfigurationStatuses.DEACTIVATEDTạm vô hiệu
ARCHIVEDConfigurationStatuses.ARCHIVEDVô hiệu vĩnh viễn

7. Tài liệu Liên quan

Tài liệuMô tả
Tổng quan & Thiết lậpKiến trúc, mô hình dữ liệu, components, services
Điều phối WebhookXử 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 CoreSchema Configuration, CryptoUtility

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.