generate static method

String generate({
  1. Bank? bank,
  2. required String accountNumber,
  3. double? amount,
  4. String? message,
  5. String? bankBin,
})

Generates a VietQR payload string compliant with NAPAS 247 specification.

Parameters:

  • bank: The beneficiary bank from the Bank enum (optional if bankBin is provided)
  • accountNumber: The beneficiary's account number
  • amount: (Optional) The transaction amount in VND. If provided, creates a dynamic QR
  • message: (Optional) A message or purpose for the transaction
  • bankBin: (Optional) Custom Bank Identification Number (6 digits). Use this for banks not in the Bank enum

Returns a string that can be used to generate a QR code.

Example:

// Static QR using Bank enum (user enters amount)
final payload = VietQR.generate(
  bank: Bank.techcombank,
  accountNumber: '9602091996',
);

// Dynamic QR using Bank enum (pre-filled amount and message)
final payload = VietQR.generate(
  bank: Bank.mbBank,
  accountNumber: '0962091996',
  amount: 150000.0,
  message: 'Thanh toan don hang',
);

// Using custom BIN for unsupported banks
final payload = VietQR.generate(
  accountNumber: '1234567890',
  bankBin: '970999', // Custom bank BIN
  amount: 100000.0,
  message: 'Payment to custom bank',
);

Implementation

static String generate({
  Bank? bank,
  required String accountNumber,
  double? amount,
  String? message,
  String? bankBin,
}) {
  // Validate inputs
  if (accountNumber.isEmpty) {
    throw ArgumentError('Account number cannot be empty');
  }

  if (amount != null && amount < 0) {
    throw ArgumentError('Amount must be greater than or equal to 0');
  }

  // Validate that either bank or bankBin is provided, but not both
  if (bank == null && bankBin == null) {
    throw ArgumentError('Either bank or bankBin must be provided');
  }

  if (bank != null && bankBin != null) {
    throw ArgumentError('Cannot provide both bank and bankBin parameters');
  }

  // Validate custom BIN format if provided
  if (bankBin != null) {
    if (bankBin.isEmpty) {
      throw ArgumentError('Bank BIN cannot be empty');
    }
    if (!RegExp(r'^\d{6}$').hasMatch(bankBin)) {
      throw ArgumentError('Bank BIN must be exactly 6 digits');
    }
  }

  final bool isDynamic = (amount != null && amount > 0) ||
      (message != null && message.isNotEmpty);

  final payload = StringBuffer();

  // Field 00: Payload Format Indicator (always "01")
  payload.write(_buildTLV(_idPayloadFormat, '01'));

  // Field 01: Point of Initiation Method
  // "11" = Static QR (user enters amount)
  // "12" = Dynamic QR (amount pre-filled)
  payload.write(_buildTLV(_idPointOfInitiation, isDynamic ? '12' : '11'));

  // Field 38: Merchant Account Information
  final binToUse = bank?.bin ?? bankBin!;
  payload.write(_buildMerchantAccountInfo(binToUse, accountNumber));

  // Field 53: Transaction Currency (704 = Vietnamese Dong)
  payload.write(_buildTLV(_idTransactionCurrency, '704'));

  // Field 54: Transaction Amount (only for dynamic QR)
  if (amount != null && amount > 0) {
    // Convert to integer (VND doesn't use decimal places)
    final amountString = amount.toInt().toString();
    payload.write(_buildTLV(_idTransactionAmount, amountString));
  }

  // Field 58: Country Code (VN = Vietnam)
  payload.write(_buildTLV(_idCountryCode, 'VN'));

  // Field 62: Additional Data Field (optional message)
  if (message != null && message.isNotEmpty) {
    payload.write(_buildAdditionalData(message));
  }

  // Field 63: CRC Checksum
  // First add the field ID and length placeholder
  payload.write('${_idCRC}04');

  // Calculate CRC on the entire payload so far
  final crc = calculateCRC16(payload.toString());
  payload.write(crc);

  return payload.toString();
}