Skip to content

Configuration & Credentials

1. Overview

The Payment service manages encrypted payment provider configurations and per-merchant credentials stored in the Configuration table (public schema). Configurations are loaded at startup to initialize MQ-Pay, while credentials are fetched on-demand per payment request. Both are encrypted with AES-256-GCM using APP_ENV_APPLICATION_SECRET and decrypted at the service layer — raw encrypted data is never exposed.

2. PaymentConfigurationService

Source: src/services/payment-configuration.service.tsExtends: BaseServiceDI Dependencies: ConfigurationRepository (via @inject) Internal: CryptoUtility.getInstance() — singleton, initialized in 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()

Signature:

typescript
async getPaymentConfiguration<ReturnType = AnyObject>(opts: {
  code: typeof SystemConfigurations.VNPAY_QR_MMS
       | typeof SystemConfigurations.VNPAY_PHONE_POS
       | string;
}): Promise<ReturnType>

Purpose: Load a provider configuration (e.g., VNPAY QR MMS connection settings) from the Configuration table, decrypt tValue, and return parsed JSON.

Flow:

Query parameters:

FilterValueSource
group200_INTEGRATIONConfigurationGroups.INTEGRATION
statusACTIVATEDConfigurationStatuses.ACTIVATED
codee.g., VNPAY_QR_MMSopts.code
environmentDEVELOPMENT or PRODUCTIONAuto-detected via applicationEnvironment.isDevelopment()

Error handling:

ConditionStatus CodeMessage
Config not found or no tValue404 NotFoundPayment configuration not found | code: {code}
Decryption fails500 InternalServerErrorFailed to decrypt payment configuration | code: {code}

Called during startup in ApplicationPaymentComponent.setupMQPay() for:

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

Both calls are made in parallel via Promise.all().

2.3. getPaymentCredential()

Signature:

typescript
async getPaymentCredential(opts: {
  provider: TMQPayProvider;
  action: TMQPayCredentialAction;
  types: TMQPayCredentialType[];
  context: IMQPayCredentialContext;
}): Promise<IMQPayCredentialResult[]>

Purpose: Query per-merchant payment credentials from Configuration table, bypassing the repository's hidden property filter to access the encrypted credential column directly via Drizzle ORM.

Flow:

IMPORTANT

This method bypasses the standard repository API by using this._configurationRepository.getConnector() to get the raw Drizzle query builder. This is necessary because credential is a hidden property in the Configuration model (hiddenProperties: ['credential', 'createdAt', 'modifiedAt', 'deletedAt']), meaning .find() and .findOne() exclude it from results.

Raw Drizzle query:

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),                           // Explicit soft-delete check
      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})`,
    ),
  );

Query filter breakdown:

FilterColumn/ExpressionValuePurpose
Groupgroup200_INTEGRATIONOnly integration configs
StatusstatusACTIVATEDOnly active configs
Principal typeprincipalTypeMerchantPer-merchant credentials
Principal IDprincipalIdcontext.merchant.idSpecific merchant
EnvironmentenvironmentDEVELOPMENT / PRODUCTIONEnv-specific
Soft deletedeletedAt IS NULLExplicit check (bypasses SoftDeletableRepository)
Metadata typemetadata->>'type'PAYMENT_PROVIDERDiscriminator
Providermetadata->>'provider'e.g., VNPAY_QR_MMSProvider filter
Credential actionmetadata->'credential'->>'action'e.g., CREATE_PAYMENTAction filter
Credential typemetadata->'credential'->>'type'ANY(ARRAY['request', 'ipn'])Type filter (multi-match)

Return type:

typescript
interface IMQPayCredentialResult<CredentialType = TNullable<SecretKeyType>> {
  provider: TMQPayProvider;        // e.g., 'VNPAY_QR_MMS'
  action: TMQPayCredentialAction;  // e.g., 'CREATE_PAYMENT'
  type: TMQPayCredentialType;      // e.g., 'request'
  credential: CredentialType;      // Decrypted credential string (nullable)
}

Error handling:

  • Missing merchant ID → throws generic error
  • Decryption failure → logs error per credential, pushes result with null credential (does not throw)
  • No results found → logs warning, returns empty array []

2.4. Credential Getter Binding

In ApplicationPaymentComponent.setupMQPay(), the service is wrapped as a credential getter callback:

typescript
this.application.bind<IMQPayOptions>({ key: MQPayBindingKeys.MQ_PAY_CLIENT_OPTIONS }).toValue({
  credentialGetter: opts => paymentConfigService.getPaymentCredential(opts),
  // ... other options
});

MQ-Pay calls this getter at runtime when it needs credentials for a specific provider action (e.g., creating a VNPAY QR payment).

3. AES-256-GCM Encryption

3.1. CryptoUtility

Source: packages/core/src/utilities/crypto.utility.tsPattern: Singleton (CryptoUtility.getInstance()) Key source: APP_ENV_APPLICATION_SECRET environment variable

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. Encryption Details

PropertyValue
AlgorithmAES-256-GCM (authenticated encryption)
Key sourceAPP_ENV_APPLICATION_SECRET
Key derivationUsed directly by IGNIS AES.withAlgorithm('aes-256-gcm')
Encrypted fieldsConfiguration.tValue (provider config), Configuration.credential (provider secrets)
Additional methodsign() — HMAC SHA-256 for webhook signature verification

3.3. Which Fields Are Encrypted

EntityFieldContainsWhen Decrypted
ConfigurationtValueProvider connection settings (JSON string)At startup via getPaymentConfiguration()
ConfigurationcredentialProvider secret keys / tokensOn-demand via getPaymentCredential()

WARNING

APP_ENV_APPLICATION_SECRET must be identical across all services that read payment configurations (Payment, Sale, etc.). A mismatch will cause AES.decrypt() to fail with a decryption error. There is no key rotation mechanism — changing the secret requires re-encrypting all stored data.

4. ApplicationPaymentComponent Init Sequence

Full initialization sequence when RUN_MODE=startup:

PhaseStepActionDI Key
Repositories1Register ConfigurationRepositoryrepositories.ConfigurationRepository
2Register WebhookConfigRepositoryrepositories.WebhookConfigRepository
3Register MigrationRepositoryrepositories.MigrationRepository
Services4Register PaymentConfigurationServiceservices.PaymentConfigurationService
5Register WebhookDispatcherServiceservices.WebhookDispatcherService
Controllers6Register WebhookConfigController
MQ-Pay7Resolve PaymentConfigurationService from DI
8Resolve WebhookConfigRepository from DI
9Resolve WebhookDispatcherService from DI
10Create WebhookEventHandlerHelper (manual new)
11Promise.all(): Load VNPAY_QR_MMS + VNPAY_PHONE_POS configs
12Bind IMQPayOptions to DI@nx-3rd/mq-pay/client-options
13Mount MQPayComponent

NOTE

When RUN_MODE is not startup (e.g., RUN_MODE=migrate), steps 7–13 are skipped entirely. Only repositories, services, and controllers are registered. This ensures migrations can run without requiring MQ-Pay infrastructure (Redis, BullMQ, VNPAY credentials).

5. Seeded Data

Source: 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. VNPAY QR MMS Seed

File: payment-0001-seed-vnpay-qr-mms-configuration

SettingValueNotes
ProviderVNPAY_QR_MMSDynamic QR code payments
CodeVNPAY_QR_MMSMatches SystemConfigurations.VNPAY_QR_MMS
Group200_INTEGRATIONConfigurationGroups.INTEGRATION
StatusACTIVATEDActive immediately
DefaulttrueDefault payment provider
ProductionfalseDevelopment environment only
App IDMERCHANTVNPAY application identifier
Master Merchant CodeA000000775VNPAY master merchant

5.2. VNPAY Phone POS Seed

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

SettingValueNotes
ProviderVNPAY_PHONE_POSNFC card payment
CodeVNPAY_PHONE_POSMatches SystemConfigurations.VNPAY_PHONE_POS
Group200_INTEGRATIONConfigurationGroups.INTEGRATION
StatusACTIVATEDActive immediately
DefaultfalseNot the default provider
ProductionfalseDevelopment environment only

6. Configuration Model Reference

6.1. Entity Definition

Source: 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;
}

Key model settings:

  • hiddenProperties: These fields are excluded from all repository .find() / .findOne() results
  • defaultFilter: Automatically adds WHERE deletedAt IS NULL to all queries (soft-delete)

6.2. ConfigurationMetadataTypes

Discriminated union system for the metadata JSONB column:

TypeConstantInterfaceDescription
PAYMENT_PROVIDERConfigurationMetadataTypes.PAYMENT_PROVIDERIPaymentProviderMetadataPayment provider credentials
typescript
interface IPaymentProviderMetadata {
  type: 'PAYMENT_PROVIDER';
  provider: string;            // TMQPayProvider (e.g., 'VNPAY_QR_MMS')
  credential: {
    action: string;            // TMQPayCredentialAction (e.g., 'CREATE_PAYMENT')
    type: string;              // TMQPayCredentialType (e.g., 'request')
  };
}

6.3. ConfigurationGroups

GroupConstantUsage
000_SYSTEMConfigurationGroups.SYSTEMSystem-level settings
100_TABLEConfigurationGroups.TABLEUI table configurations
200_INTEGRATIONConfigurationGroups.INTEGRATIONPayment providers, external integrations

6.4. ConfigurationStatuses

StatusConstantDescription
ACTIVATEDConfigurationStatuses.ACTIVATEDActive — used in queries
DEACTIVATEDConfigurationStatuses.DEACTIVATEDTemporarily disabled
ARCHIVEDConfigurationStatuses.ARCHIVEDPermanently disabled
DocumentDescription
Overview & SetupArchitecture, domain model, components, services
Webhook DispatchEvent handling, retry logic, payload format
DeploymentTraefik routing, Docker, health checks
Core PackageConfiguration schema, CryptoUtility source

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