feat: implement cash drawer security gate to conditionally trigger drawer pulses based on payment type

This commit is contained in:
Suherdy Yacob 2026-06-03 11:16:41 +07:00
parent 5524546dab
commit 2397c6dcb3
2 changed files with 100 additions and 3 deletions

View File

@ -433,6 +433,29 @@ export class EscPosGenerator {
return new Uint8Array(cmds);
}
/**
* Strip ESC/POS cash drawer pulse commands (ESC p / 0x1B 0x70) from Uint8Array data.
*
* @param {Uint8Array} data
* @returns {Uint8Array}
*/
static stripCashDrawerCommands(data) {
if (!data || data.length === 0) return new Uint8Array(0);
const cleanData = [];
let i = 0;
while (i < data.length) {
// Check for ESC p (0x1B 0x70)
if (data[i] === 0x1B && data[i + 1] === 0x70) {
// ESC p m t1 t2 has 5 bytes
i += 5;
} else {
cleanData.push(data[i]);
i++;
}
}
return new Uint8Array(cleanData);
}
/**
* Helper: combine multiple Uint8Arrays into one.
*/

View File

@ -107,6 +107,25 @@ patch(PosPrinterService.prototype, {
console.log('[BluetoothPrint] Element tag:', el?.tagName);
console.log('[BluetoothPrint] Element classes:', el?.className);
// Determine print context
const pos = this.env?.services?.pos;
const currentPrintContext = pos?.hardwareProxy?.currentPrintContext;
const order = this._currentPrintOrder || currentPrintContext?.order || pos?.get_order();
let printType = currentPrintContext?.printType;
if (!printType) {
if (this._currentPrintBasic) {
printType = 'CHECKER';
} else if (el && el.querySelector && el.querySelector('.new-changes, .preset-name, .o-employee-name, .order-ref-prefix')) {
printType = 'CHECKER';
} else {
printType = 'PAYMENT';
}
}
this._isCashPayment = printType === 'PAYMENT' && order && (typeof order.isPaidWithCash === 'function' ? order.isPaidWithCash() : false);
console.log(`[BluetoothPrint] printHtml context check - printType: ${printType}, order: ${order?.pos_reference}, resolved isCashPayment: ${this._isCashPayment}`);
// Check if a Bluetooth printer is configured (from localStorage)
const storage = new BluetoothPrinterStorage();
@ -229,6 +248,7 @@ patch(PosPrinterService.prototype, {
} finally {
this._currentPrintOrder = null;
this._currentPrintBasic = false;
this._isCashPayment = false;
}
},
@ -380,6 +400,51 @@ patch(PosPrinterService.prototype, {
});
},
/**
* Apply security gate on ESC/POS data:
* - Strips cash drawer pulse commands if isCashPayment is false.
* - Appends cash drawer pulse commands if isCashPayment is true.
*
* @private
* @param {Uint8Array} escposData - ESC/POS byte sequence
* @param {boolean} isCashPayment - True if context is cash payment print
* @returns {Uint8Array}
*/
_applyCashDrawerSecurityGate(escposData, isCashPayment) {
if (!escposData) return escposData;
// 1. Strip any existing cash drawer pulse commands (0x1B 0x70) to prevent unauthorized triggers
const initialLen = escposData.length;
const cleanData = EscPosGenerator.stripCashDrawerCommands(escposData);
const strippedCount = Math.floor((initialLen - cleanData.length) / 5);
if (strippedCount > 0) {
console.log(`[BluetoothPrint] Stripped ${strippedCount} ESC p command(s) from print stream.`);
}
let finalData = cleanData;
// 2. If it is a cash payment print context, inject the cash drawer pulse command
if (isCashPayment) {
console.log('[BluetoothPrint] Cash payment context: Appending cash drawer pulse commands to ESC/POS stream.');
// Standard ESC/POS cashbox open commands (pin 2 and pin 5)
const pulseCmds = new Uint8Array([
0x1B, 0x3D, 0x01, // ESC = 1 (Select printer)
0x1B, 0x70, 0x00, 0x19, 0x19, // ESC p 0 25 25 (pulse pin 2)
0x1B, 0x70, 0x01, 0x19, 0x19 // ESC p 1 25 25 (pulse pin 5)
]);
// Combine pulse commands with the clean receipt data
const combined = new Uint8Array(pulseCmds.length + finalData.length);
combined.set(pulseCmds, 0);
combined.set(finalData, pulseCmds.length);
finalData = combined;
} else {
console.log('[BluetoothPrint] Non-cash print context: Cash drawer pulse blocked.');
}
return finalData;
},
/**
* Print receipt via bluetooth thermal printer from HTML element
* Uses graphics mode to print exact HTML layout
@ -433,9 +498,12 @@ patch(PosPrinterService.prototype, {
console.log('[BluetoothPrint] EscPosGraphics created successfully');
console.log('[BluetoothPrint] Generating bitmap commands...');
const escposData = graphicsGenerator.generateBitmapCommands(bitmap);
let escposData = graphicsGenerator.generateBitmapCommands(bitmap);
console.log('[BluetoothPrint] Generated', escposData.length, 'bytes of graphics data');
// Apply cash drawer security gate
escposData = this._applyCashDrawerSecurityGate(escposData, this._isCashPayment);
// Send data to printer
console.log('[BluetoothPrint] Sending graphics to printer...');
const startTime = performance.now();
@ -498,9 +566,12 @@ patch(PosPrinterService.prototype, {
// Generate ESC/POS commands
console.log('[BluetoothPrint] Generating ESC/POS text commands...');
const escposData = escposGenerator.generateReceipt(receiptData);
let escposData = escposGenerator.generateReceipt(receiptData);
console.log('[BluetoothPrint] Generated', escposData.length, 'bytes of text data');
// Apply cash drawer security gate
escposData = this._applyCashDrawerSecurityGate(escposData, this._isCashPayment);
// Send data to printer
console.log('[BluetoothPrint] Sending text to printer...');
await bluetoothManager.sendData(escposData, false);
@ -538,9 +609,12 @@ patch(PosPrinterService.prototype, {
// Generate ESC/POS commands
console.log('[BluetoothPrint] Generating ESC/POS commands...');
const escposData = escposGenerator.generateReceipt(receiptData);
let escposData = escposGenerator.generateReceipt(receiptData);
console.log('[BluetoothPrint] Generated', escposData.length, 'bytes of ESC/POS data');
// Apply cash drawer security gate
escposData = this._applyCashDrawerSecurityGate(escposData, this._isCashPayment);
// Send data to printer
console.log('[BluetoothPrint] Sending to printer...');
await bluetoothManager.sendData(escposData, false);