# Meezi — Printer Support Plan > Copy-paste this into Cursor. > Stack: ASP.NET Core 10, Next.js 14, Flutter 3 > Existing packages: QuestPDF 2024.12.3, QRCoder 1.6.0 --- ## Architecture Overview ``` Three print paths — build all three: PATH 1: Network Printer (API → TCP → Printer) Browser/Mobile → POST /api/print/receipt → API → TCP:9100 → Thermal printer Use for: POS receipts, kitchen tickets, table bills Requires: WiFi/Ethernet printer on same LAN as server PATH 2: PDF via QuestPDF (API → PDF → Download/Print) Browser → GET /api/print/report/{id}.pdf → stream PDF → browser print dialog Use for: End-of-day reports, formal invoices, management summaries Requires: Nothing extra — QuestPDF already installed PATH 3: QZ Tray Bridge (Browser WebSocket → localhost:8181 → USB Printer) Browser → WebSocket localhost:8181 → QZ Tray → USB thermal printer Use for: Cafés with USB-only printers (cheaper hardware) Requires: QZ Tray installed on café's Windows machine (one-time setup) ``` --- ## PROMPT 1 — Network Thermal Printer: Backend ESC/POS Service ``` Context: Meezi POS, ASP.NET Core 10. BranchSettings entity exists. Goal: Print thermal receipts by sending ESC/POS bytes over TCP to a network printer. No extra library needed — raw TCP socket. ──────────────────────────────────────────────────────────────── STEP 1 — Add printer config to BranchSettings ──────────────────────────────────────────────────────────────── File: src/Meezi.Core/Entities/BranchSettings.cs — add fields: // Network printer config (TCP/IP) public string? ReceiptPrinterIp { get; set; } // e.g. "192.168.1.100" public int? ReceiptPrinterPort { get; set; } // default 9100 public string? KitchenPrinterIp { get; set; } // separate kitchen printer public int? KitchenPrinterPort { get; set; } public int PaperWidthMm { get; set; } = 80; // 58 or 80 public bool AutoCutEnabled { get; set; } = true; public string? ReceiptLogoBase64 { get; set; } // optional small logo EF migration: dotnet ef migrations add AddPrinterSettings \ --project src/Meezi.Infrastructure \ --startup-project src/Meezi.API ──────────────────────────────────────────────────────────────── STEP 2 — ESC/POS builder utility ──────────────────────────────────────────────────────────────── File: src/Meezi.API/Services/Printing/EscPosBuilder.cs (new) Do NOT use any external ESC/POS library. Build the byte sequences directly. ESC/POS is a simple byte protocol — only need these commands: public class EscPosBuilder { private readonly List _buffer = new(); // ESC/POS constants private static readonly byte[] ESC = { 0x1B }; private static readonly byte[] GS = { 0x1D }; public EscPosBuilder Initialize() { // ESC @ — initialize printer _buffer.AddRange(new byte[] { 0x1B, 0x40 }); return this; } public EscPosBuilder SetEncoding() { // ESC t 37 — set code page to UTF-8 compatible // For Persian: use code page that supports UTF-8 // Most modern Epson/Bixolon support ESC t with PC720 or UTF-8 _buffer.AddRange(new byte[] { 0x1B, 0x74, 0x25 }); return this; } public EscPosBuilder AlignCenter() { _buffer.AddRange(new byte[] { 0x1B, 0x61, 0x01 }); return this; } public EscPosBuilder AlignRight() { _buffer.AddRange(new byte[] { 0x1B, 0x61, 0x02 }); return this; } public EscPosBuilder AlignLeft() { _buffer.AddRange(new byte[] { 0x1B, 0x61, 0x00 }); return this; } public EscPosBuilder Bold(bool on) { _buffer.AddRange(new byte[] { 0x1B, 0x45, on ? (byte)1 : (byte)0 }); return this; } public EscPosBuilder DoubleHeight(bool on) { _buffer.AddRange(new byte[] { 0x1B, 0x21, on ? (byte)0x10 : (byte)0x00 }); return this; } public EscPosBuilder Text(string text) { // Encode as UTF-8 — modern thermal printers support it _buffer.AddRange(System.Text.Encoding.UTF8.GetBytes(text)); return this; } public EscPosBuilder Line(string text = "") { return Text(text + "\n"); } public EscPosBuilder Separator(int width = 48, char ch = '-') { return Line(new string(ch, width)); } // Print two columns (right-aligned second column) // e.g. "کالا × 2" and "25,000 تومان" on same line public EscPosBuilder TwoColumns(string left, string right, int totalWidth = 48) { var padded = left.PadRight(totalWidth - right.Length - 1) + right; return Line(padded); } public EscPosBuilder Feed(int lines = 3) { // GS V — feed and cut (lines before cut) for (int i = 0; i < lines; i++) _buffer.Add(0x0A); return this; } public EscPosBuilder Cut() { // GS V 66 3 — partial cut with feed _buffer.AddRange(new byte[] { 0x1D, 0x56, 0x42, 0x03 }); return this; } public byte[] Build() => _buffer.ToArray(); } ──────────────────────────────────────────────────────────────── STEP 3 — Receipt template builder ──────────────────────────────────────────────────────────────── File: src/Meezi.API/Services/Printing/ReceiptBuilder.cs (new) Uses EscPosBuilder to compose a full receipt from an OrderDto + BranchSettings. public class ReceiptBuilder { public byte[] BuildReceipt(Order order, BranchEffectiveSettingsDto settings, string cafeName, string branchName) { var b = new EscPosBuilder(); int width = settings.PaperWidthMm == 58 ? 32 : 48; b.Initialize() .SetEncoding(); // Header b.AlignCenter() .Bold(true) .DoubleHeight(true) .Line(cafeName) .DoubleHeight(false) .Bold(false) .Line(branchName); if (!string.IsNullOrEmpty(settings.ReceiptHeader)) b.Line(settings.ReceiptHeader); // Order info var shamsiDate = ToShamsi(order.CreatedAt); // helper method b.AlignRight() .Line($"شماره سفارش: {order.OrderNumber}") .Line($"تاریخ: {shamsiDate}") .Line($"میز: {order.TableName ?? "—"}"); if (!string.IsNullOrEmpty(order.GuestName)) b.Line($"مهمان: {order.GuestName}"); b.Separator(width) .AlignRight(); // Items foreach (var item in order.Items.Where(i => !i.IsVoided)) { var itemTotal = FormatCurrency(item.UnitPrice * item.Quantity); var itemLine = $"{item.ProductName} × {item.Quantity}"; b.TwoColumns(itemLine, itemTotal, width); } b.Separator(width); // Totals if (order.TaxAmount > 0) b.TwoColumns("مالیات", FormatCurrency(order.TaxAmount), width); if (order.ServiceCharge > 0) b.TwoColumns("سرویس", FormatCurrency(order.ServiceCharge), width); b.Bold(true) .TwoColumns("مجموع کل", FormatCurrency(order.TotalAmount), width) .Bold(false); // Payments foreach (var payment in order.Payments ?? []) { var methodLabel = payment.Method switch { "Cash" => "نقد", "Card" => "کارت", "Credit" => "اعتبار", _ => payment.Method }; b.TwoColumns(methodLabel, FormatCurrency(payment.Amount), width); } b.Separator(width); // Footer b.AlignCenter(); if (!string.IsNullOrEmpty(settings.WifiPassword)) b.Line($"WiFi: {settings.WifiPassword}"); if (!string.IsNullOrEmpty(settings.ReceiptFooter)) b.Line(settings.ReceiptFooter); b.Line("ممنون از انتخاب شما"); b.Feed(3) .Cut(); return b.Build(); } private static string FormatCurrency(decimal amount) => $"{amount:N0} تومان"; private static string ToShamsi(DateTime dt) { // Use System.Globalization.PersianCalendar var pc = new System.Globalization.PersianCalendar(); return $"{pc.GetYear(dt)}/{pc.GetMonth(dt):D2}/{pc.GetDayOfMonth(dt):D2} " + $"{dt:HH:mm}"; } } ──────────────────────────────────────────────────────────────── STEP 4 — Network printer sender ──────────────────────────────────────────────────────────────── File: src/Meezi.API/Services/Printing/NetworkPrinterService.cs (new) public interface IPrinterService { Task PrintReceiptAsync(Guid orderId, CancellationToken ct); Task PrintKitchenTicketAsync(Guid orderId, CancellationToken ct); Task TestPrintAsync(string printerIp, int port, CancellationToken ct); } public class NetworkPrinterService : IPrinterService { private readonly IOrderService _orders; private readonly IEffectiveSettingsService _settings; private readonly ReceiptBuilder _receiptBuilder; private readonly ILogger _logger; public async Task PrintReceiptAsync(Guid orderId, CancellationToken ct) { var order = await _orders.GetOrderAsync(orderId, ct); var settings = await _settings.GetEffectiveSettingsAsync( order.CafeId, order.BranchId, ct); if (string.IsNullOrEmpty(settings.ReceiptPrinterIp)) return PrintResult.Fail("PRINTER_NOT_CONFIGURED"); var bytes = _receiptBuilder.BuildReceipt(order, settings, order.CafeName, order.BranchName); return await SendToPrinterAsync( settings.ReceiptPrinterIp, settings.ReceiptPrinterPort ?? 9100, bytes, ct); } public async Task PrintKitchenTicketAsync(Guid orderId, CancellationToken ct) { var order = await _orders.GetOrderAsync(orderId, ct); var settings = await _settings.GetEffectiveSettingsAsync( order.CafeId, order.BranchId, ct); if (string.IsNullOrEmpty(settings.KitchenPrinterIp)) return PrintResult.Fail("KITCHEN_PRINTER_NOT_CONFIGURED"); var bytes = BuildKitchenTicket(order, settings); return await SendToPrinterAsync( settings.KitchenPrinterIp, settings.KitchenPrinterPort ?? 9100, bytes, ct); } private static byte[] BuildKitchenTicket(Order order, BranchEffectiveSettingsDto settings) { var b = new EscPosBuilder(); int width = settings.PaperWidthMm == 58 ? 32 : 48; var pc = new System.Globalization.PersianCalendar(); b.Initialize() .SetEncoding() .AlignCenter() .Bold(true) .DoubleHeight(true) .Line("آشپزخانه") .DoubleHeight(false) .AlignRight() .Line($"میز: {order.TableName ?? "—"} | #{order.OrderNumber}") .Line($"{DateTime.Now:HH:mm}") .Separator(width); foreach (var item in order.Items.Where(i => !i.IsVoided)) { b.Bold(true) .Line($"× {item.Quantity} {item.ProductName}") .Bold(false); if (!string.IsNullOrEmpty(item.Notes)) b.Line($" ← {item.Notes}"); } b.Feed(4).Cut(); return b.Build(); } private async Task SendToPrinterAsync( string ip, int port, byte[] data, CancellationToken ct) { try { using var client = new System.Net.Sockets.TcpClient(); client.SendTimeout = 3000; client.ReceiveTimeout = 3000; await client.ConnectAsync(ip, port, ct); await using var stream = client.GetStream(); await stream.WriteAsync(data, ct); await stream.FlushAsync(ct); _logger.LogInformation("Printed {Bytes} bytes to {Ip}:{Port}", data.Length, ip, port); return PrintResult.Ok(); } catch (Exception ex) { _logger.LogError(ex, "Print failed to {Ip}:{Port}", ip, port); return PrintResult.Fail("PRINTER_CONNECTION_FAILED", ex.Message); } } } public record PrintResult(bool Success, string? ErrorCode, string? ErrorDetail) { public static PrintResult Ok() => new(true, null, null); public static PrintResult Fail(string code, string? detail = null) => new(false, code, detail); } ──────────────────────────────────────────────────────────────── STEP 5 — Print controller ──────────────────────────────────────────────────────────────── File: src/Meezi.API/Controllers/PrintController.cs (new) Base: CafeApiControllerBase (inherits [Authorize]) POST /api/cafes/{cafeId}/print/receipt/{orderId} → Prints receipt to branch receipt printer → Returns 200 OK or ApiError with PRINTER_NOT_CONFIGURED / PRINTER_CONNECTION_FAILED POST /api/cafes/{cafeId}/print/kitchen/{orderId} → Prints kitchen ticket to branch kitchen printer POST /api/cafes/{cafeId}/print/test Body: { printerIp, port } → Prints test page: "Meezi Test Print ✓" + date → Owner/Manager only — used from settings page to verify connection ──────────────────────────────────────────────────────────────── STEP 6 — Register services in DI ──────────────────────────────────────────────────────────────── File: src/Meezi.API/Extensions/ServiceCollectionExtensions.cs services.AddScoped(); services.AddScoped(); services.AddScoped(); ──────────────────────────────────────────────────────────────── STEP 7 — Auto-print after payment ──────────────────────────────────────────────────────────────── In OrderService.ProcessPaymentAsync, after order is closed: // Fire-and-forget print — don't block payment on print success _ = Task.Run(async () => { try { await _printerService.PrintReceiptAsync(order.Id, CancellationToken.None); } catch (Exception ex) { _logger.LogWarning(ex, "Auto-print failed for order {OrderId}", order.Id); } }); // Also print kitchen ticket when new items are added (on append) // In OrderService.AppendItemsAsync — after saving: _ = Task.Run(async () => { try { await _printerService.PrintKitchenTicketAsync(order.Id, CancellationToken.None); } catch (Exception ex) { _logger.LogWarning(ex, "Kitchen print failed for order {OrderId}", order.Id); } }); ──────────────────────────────────────────────────────────────── STEP 8 — Dashboard: Print button + printer settings ──────────────────────────────────────────────────────────────── In pos-pay-panel.tsx — after payment success, add manual print button: Where printReceipt calls: POST /api/cafes/{cafeId}/print/receipt/{orderId} Show toast on success/failure: Success: t("print.success") — "رسید چاپ شد" Failure PRINTER_NOT_CONFIGURED: t("print.notConfigured") — "پرینتر تنظیم نشده" Failure PRINTER_CONNECTION_FAILED: t("print.connectionFailed") — "خطا در اتصال به پرینتر" In settings page (web/dashboard/src/components/settings/): Add "تنظیمات پرینتر" section: - Receipt printer IP input - Receipt printer port (default 9100) - Kitchen printer IP input - Paper width selector (58mm / 80mm) - Auto-cut toggle - WiFi password field (shown on receipt) - "تست پرینت" button → POST /print/test ──────────────────────────────────────────────────────────────── TESTS (add to tests/Meezi.API.Tests/PrintingTests.cs) ──────────────────────────────────────────────────────────────── For unit tests, mock IPrinterService — don't test actual TCP. Test ReceiptBuilder and EscPosBuilder directly (they're pure logic). ✓ ReceiptBuilder_ExcludesVoidedItems ✓ ReceiptBuilder_AppliesPersianCalendarDate ✓ ReceiptBuilder_ShowsTaxLine_WhenTaxNonZero ✓ ReceiptBuilder_80mm_Uses48CharWidth ✓ ReceiptBuilder_58mm_Uses32CharWidth ✓ KitchenTicket_IncludesItemNotes ✓ PrintController_NoPrinterConfigured_ReturnsPrinterNotConfigured ✓ PrintController_AfterPayment_AutoPrintFires (mock IPrinterService) ✓ EscPosBuilder_Cut_AppendsCorrectBytes ✓ EscPosBuilder_TwoColumns_PadsCorrectly i18n strings: fa.json under "print": { "printReceipt": "چاپ رسید", "printKitchen": "ارسال به آشپزخانه", "success": "رسید با موفقیت چاپ شد", "notConfigured": "آدرس پرینتر تنظیم نشده است", "connectionFailed": "خطا در اتصال به پرینتر", "testPrint": "تست پرینت", "printerSettings": "تنظیمات پرینتر", "receiptPrinter": "پرینتر رسید", "kitchenPrinter": "پرینتر آشپزخانه", "paperWidth": "عرض کاغذ", "autoCut": "برش خودکار" } ``` --- ## PROMPT 2 — PDF Formal Invoices via QuestPDF ``` Context: Meezi POS, ASP.NET Core 10. QuestPDF 2024.12.3 already installed. Goal: Generate professional PDF invoices for orders and end-of-day reports. These are for formal billing, accounting export, and management — not thermal. ──────────────────────────────────────────────────────────────── STEP 1 — Order Invoice PDF ──────────────────────────────────────────────────────────────── File: src/Meezi.API/Services/Printing/OrderInvoiceDocument.cs (new) Use QuestPDF fluent API to build an A4 invoice. Key sections: Header: - Café logo (if stored) or café name large - Branch name, address, phone - "فاکتور رسمی" title - Invoice number (= order number), date in Shamsi Customer section: - Guest name / customer name if linked - Table number, server name Items table: Columns: ردیف | نام آیتم | تعداد | قیمت واحد | جمع - Voided items excluded - Alternating row shading Totals section: - Subtotal - Tax (if applicable) - Service charge (if applicable) - Total (bold, larger) - Payment method(s) Footer: - Thank you message - WiFi password if set - QR code linking to digital receipt (use QRCoder) QuestPDF document class: public class OrderInvoiceDocument : IDocument { private readonly Order _order; private readonly BranchEffectiveSettingsDto _settings; private readonly string _cafeName; public DocumentMetadata GetMetadata() => DocumentMetadata.Default with { Title = $"فاکتور #{_order.OrderNumber}", Author = _cafeName }; public void Compose(IDocumentContainer container) { container.Page(page => { page.Size(PageSizes.A4); page.MarginHorizontal(30); page.MarginVertical(20); page.ContentFromRightToLeft(); // RTL for Persian page.Header().Element(ComposeHeader); page.Content().Element(ComposeContent); page.Footer().Element(ComposeFooter); }); } // Implement ComposeHeader, ComposeContent, ComposeFooter // following QuestPDF fluent API patterns } ──────────────────────────────────────────────────────────────── STEP 2 — Daily Report PDF ──────────────────────────────────────────────────────────────── File: src/Meezi.API/Services/Printing/DailyReportDocument.cs (new) A4 PDF for end-of-day / end-of-shift report. Sections: Header: café name, branch, date, shift info KPI summary: total revenue, orders, avg order, net income — as large stat boxes Payment breakdown: Cash / Card / Credit as a simple table Top 10 products: table with rank, name, qty, revenue Expense list: category, amount, note Shift reconciliation: opening cash, expected, actual, discrepancy Staff signature line (for physical printing) ──────────────────────────────────────────────────────────────── STEP 3 — PDF endpoints ──────────────────────────────────────────────────────────────── File: src/Meezi.API/Controllers/PrintController.cs — add: GET /api/cafes/{cafeId}/print/invoice/{orderId}.pdf → Generates and streams PDF → Content-Type: application/pdf → Content-Disposition: inline (opens in browser) or attachment (downloads) → Query param: ?disposition=inline|attachment → Authorization: any authenticated staff GET /api/cafes/{cafeId}/reports/daily/{date}/pdf?branchId= → Generates DailyReport PDF for given date → Authorization: Manager+ Implementation: var pdf = Document.Create(doc => new OrderInvoiceDocument(order, settings, cafeName).Compose(doc)); var bytes = pdf.GeneratePdf(); return File(bytes, "application/pdf", $"invoice-{order.OrderNumber}.pdf"); ──────────────────────────────────────────────────────────────── STEP 4 — Dashboard PDF download buttons ──────────────────────────────────────────────────────────────── In pos-receipt-modal.tsx — add second button: In reports page — add "دانلود PDF" button per daily report row: → uses lib/api/download.ts downloadFile() helper i18n additions: fa.json: "print.formalInvoice": "فاکتور رسمی (PDF)" "print.downloadReport": "دانلود گزارش PDF" ``` --- ## PROMPT 3 — QZ Tray Bridge (USB Printer Support) ``` Context: Meezi dashboard (Next.js 14). Some cafés have USB thermal printers. Goal: Support USB printing via QZ Tray — a background service the café installs once. The dashboard communicates with it via WebSocket on localhost:8181. ──────────────────────────────────────────────────────────────── STEP 1 — Install QZ Tray client library ──────────────────────────────────────────────────────────────── cd web/dashboard npm install qz-tray ──────────────────────────────────────────────────────────────── STEP 2 — QZ Tray print service ──────────────────────────────────────────────────────────────── File: web/dashboard/src/lib/printing/qz-bridge.ts (new) import qz from "qz-tray"; let connected = false; export async function connectQZ(): Promise { if (connected) return true; try { await qz.websocket.connect(); connected = true; return true; } catch { return false; // QZ Tray not running — fall back to API print } } export async function disconnectQZ() { if (connected) { await qz.websocket.disconnect(); connected = false; } } export async function printWithQZ( printerName: string, escPosHex: string[] // hex strings of ESC/POS bytes from API ): Promise { const ok = await connectQZ(); if (!ok) return false; const config = qz.configs.create(printerName); const data = escPosHex.map(hex => ({ type: "raw" as const, format: "hex" as const, data: hex })); await qz.print(config, data); return true; } export async function listQZPrinters(): Promise { const ok = await connectQZ(); if (!ok) return []; return await qz.printers.find(); } ──────────────────────────────────────────────────────────────── STEP 3 — Backend: add hex export endpoint ──────────────────────────────────────────────────────────────── File: src/Meezi.API/Controllers/PrintController.cs — add: GET /api/cafes/{cafeId}/print/receipt/{orderId}/raw → Same as receipt builder, but instead of sending to TCP printer, returns the ESC/POS bytes as hex string array in JSON → Used by QZ Tray bridge in browser Response: { "printerName": "EPSON TM-T88VI", // from branch settings "data": ["1b40", "1b74", ...] // ESC/POS bytes as hex } In BranchSettings, add: public string? UsbPrinterName { get; set; } // Windows printer name for QZ ──────────────────────────────────────────────────────────────── STEP 4 — Smart print dispatcher in dashboard ──────────────────────────────────────────────────────────────── File: web/dashboard/src/lib/printing/print-dispatcher.ts (new) Tries QZ Tray first, falls back to API network print. export async function printReceipt( cafeId: string, orderId: string ): Promise<{ success: boolean; method: "qz" | "network" | "failed" }> { // Try QZ Tray first (USB printer) const qzOk = await connectQZ(); if (qzOk) { try { const res = await apiClient.get( `/cafes/${cafeId}/print/receipt/${orderId}/raw` ); const printed = await printWithQZ(res.printerName, res.data); if (printed) return { success: true, method: "qz" }; } catch { // fall through } } // Fall back to network print via API try { await apiClient.post(`/cafes/${cafeId}/print/receipt/${orderId}`); return { success: true, method: "network" }; } catch { return { success: false, method: "failed" }; } } ──────────────────────────────────────────────────────────────── STEP 5 — Printer settings page: detect QZ Tray ──────────────────────────────────────────────────────────────── In settings page printer section, add a QZ Tray detector: const [qzAvailable, setQzAvailable] = useState(null); const [availablePrinters, setAvailablePrinters] = useState([]); useEffect(() => { connectQZ().then(ok => { setQzAvailable(ok); if (ok) listQZPrinters().then(setAvailablePrinters); }); }, []); UI: if qzAvailable === true: → Green badge: "QZ Tray متصل است ✓" → Dropdown: select USB printer from availablePrinters → Save selection to branchSettings.UsbPrinterName if qzAvailable === false: → Yellow badge: "QZ Tray نصب نشده" → Link: "دانلود QZ Tray" → https://qz.io → Info: "برای استفاده از پرینترهای USB، QZ Tray را یک‌بار روی این کامپیوتر نصب کنید" → Alternative: "در صورت داشتن پرینتر شبکه، آدرس IP را وارد کنید" if qzAvailable === null: → Spinner: "در حال بررسی..." ``` --- ## PROMPT 4 — Flutter Bluetooth/Network Print (meezi_pos) ``` Context: Flutter 3, mobile/meezi_pos. Goal: Print from the Flutter POS app to Bluetooth or network thermal printers. ──────────────────────────────────────────────────────────────── STEP 1 — Add packages to pubspec.yaml ──────────────────────────────────────────────────────────────── dependencies: esc_pos_utils_plus: ^2.0.2 # ESC/POS command builder for Dart flutter_bluetooth_printer: ^3.0.0 # Bluetooth printing (Android/iOS) # For network printing, use dart:io TcpSocket directly (no extra package) ──────────────────────────────────────────────────────────────── STEP 2 — Print service abstraction ──────────────────────────────────────────────────────────────── File: mobile/meezi_pos/lib/services/print_service.dart abstract class PrintService { Future printReceipt(OrderModel order, BranchSettings settings); Future printKitchenTicket(OrderModel order); Future> discoverPrinters(); } class NetworkPrintService implements PrintService { @override Future printReceipt(OrderModel order, BranchSettings settings) async { final bytes = _buildReceiptBytes(order, settings); return await _sendToNetworkPrinter( settings.receiptPrinterIp!, settings.receiptPrinterPort ?? 9100, bytes ); } Future _sendToNetworkPrinter(String ip, int port, List bytes) async { try { final socket = await Socket.connect(ip, port, timeout: const Duration(seconds: 3)); socket.add(bytes); await socket.flush(); await socket.close(); return true; } catch (e) { debugPrint("Network print error: $e"); return false; } } } class BluetoothPrintService implements PrintService { @override Future printReceipt(OrderModel order, BranchSettings settings) async { // Use flutter_bluetooth_printer // Find paired printer → send ESC/POS bytes final bytes = _buildReceiptBytes(order, settings); return await FlutterBluetoothPrinter.printBytes( address: settings.bluetoothPrinterAddress!, data: Uint8List.fromList(bytes), ); } @override Future> discoverPrinters() async { return await FlutterBluetoothPrinter.discover(); } } ──────────────────────────────────────────────────────────────── STEP 3 — ESC/POS receipt builder in Dart ──────────────────────────────────────────────────────────────── File: mobile/meezi_pos/lib/services/receipt_builder.dart Use esc_pos_utils_plus to build the receipt — same structure as the C# ReceiptBuilder (header, items, totals, footer, cut). PaperSize based on settings.paperWidthMm: 58mm → PaperSize.mm58 80mm → PaperSize.mm80 Persian text: most modern BT printers support UTF-8. If printer doesn't support Persian, fall back to transliteration or use image-mode printing (render receipt as image, print as bitmap). ──────────────────────────────────────────────────────────────── STEP 4 — Printer settings in Flutter POS ──────────────────────────────────────────────────────────────── File: mobile/meezi_pos/lib/screens/printer_settings_screen.dart Tabs: 1. "بلوتوث" — scan + pair Bluetooth printers 2. "شبکه" — enter IP:port manually or auto-discover via mDNS On pair/save → store in SharedPreferences or Drift local DB. Add "تست پرینت" button — prints test page. ──────────────────────────────────────────────────────────────── STEP 5 — Auto-print after POS payment ──────────────────────────────────────────────────────────────── In order payment flow, after API confirms payment: final printService = ref.read(printServiceProvider); final printed = await printService.printReceipt(order, branchSettings); if (!printed) { showSnackBar(context, "خطا در اتصال به پرینتر"); } ``` --- ## Summary: Which Approach for Which Scenario | Scenario | Solution | Effort | |----------|----------|--------| | Café has WiFi/Ethernet printer | PROMPT 1 (API → TCP) | ⭐ Build first | | Need formal A4 invoice PDF | PROMPT 2 (QuestPDF) | ⭐ Build second | | Café has USB printer on Windows PC | PROMPT 3 (QZ Tray) | Build third | | Mobile waiter tablet → Bluetooth print | PROMPT 4 (Flutter) | Flutter sprint | ## New Error Codes | Code | Meaning | |------|---------| | `PRINTER_NOT_CONFIGURED` | No printer IP set in branch settings | | `KITCHEN_PRINTER_NOT_CONFIGURED` | No kitchen printer IP set | | `PRINTER_CONNECTION_FAILED` | TCP connection to printer failed | | `PRINTER_TIMEOUT` | Printer connected but didn't respond | ## Recommended Printers for Iran (tested with ESC/POS + TCP/9100) - **Epson TM-T82III-i** — built-in WiFi, widely available, best support - **Bixolon SRP-350plusIII** — network version, very reliable - **Sewoo LK-TE112NR** — cheaper, good for budget cafés - **80mm paper** recommended over 58mm — more readable --- *Start with PROMPT 1 — it has zero dependencies and works immediately with any network printer.*