From 2397c6dcb3a41b7b9d3d8c2373d7248318bbec5f Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Wed, 3 Jun 2026 11:16:41 +0700 Subject: [PATCH] feat: implement cash drawer security gate to conditionally trigger drawer pulses based on payment type --- static/src/js/escpos_generator.js | 23 ++++++++ static/src/js/pos_receipt_printer.js | 80 ++++++++++++++++++++++++++-- 2 files changed, 100 insertions(+), 3 deletions(-) diff --git a/static/src/js/escpos_generator.js b/static/src/js/escpos_generator.js index 820b002..e2e5d9f 100755 --- a/static/src/js/escpos_generator.js +++ b/static/src/js/escpos_generator.js @@ -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. */ diff --git a/static/src/js/pos_receipt_printer.js b/static/src/js/pos_receipt_printer.js index 88b4c0e..f8847e5 100755 --- a/static/src/js/pos_receipt_printer.js +++ b/static/src/js/pos_receipt_printer.js @@ -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);