generate static method
Generates a VietQR payload string compliant with NAPAS 247 specification.
Parameters:
bank
: The beneficiary bank from the Bank enum (optional ifbankBin
is provided)accountNumber
: The beneficiary's account numberamount
: (Optional) The transaction amount in VND. If provided, creates a dynamic QRmessage
: (Optional) A message or purpose for the transactionbankBin
: (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();
}