@teincfood/core 0.7.7 → 0.7.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Printer service — fetch receipt from backend when online,
3
+ * otherwise generate locally from SellerOrder snapshot (backend-identical template).
4
+ * Sends ESC/POS via TcpSocketAdapter, falls back to PrintFileAdapter.
5
+ */
6
+ import { getHttpClient } from "../adapters/http";
7
+ import { getTcpAdapter, getPrintFileAdapter } from "../adapters/print";
8
+ import { getPrinterState } from "./store";
9
+ import { generateEscpos, generateHtml } from "./receipt";
10
+ async function fetchReceipt(orderId, format = "html") {
11
+ const http = getHttpClient();
12
+ // http adapter wraps axios; fallback to fetch if not set
13
+ if (http) {
14
+ const res = await http.get(`/orders/${orderId}/receipt?format=${format}`, {
15
+ headers: { Accept: format === "html" ? "text/html" : "text/plain" },
16
+ });
17
+ // http.get may return { data: string } or raw string; handle both
18
+ const data = res.data ?? res;
19
+ if (typeof data === "string")
20
+ return data;
21
+ return String(data);
22
+ }
23
+ // Fallback fetch using API_BASE_URL already injected
24
+ const { API_BASE_URL } = await import("../utils/constants");
25
+ const url = `${API_BASE_URL}/orders/${orderId}/receipt?format=${format}`;
26
+ const resp = await fetch(url, { headers: { Accept: format === "html" ? "text/html" : "text/plain" } });
27
+ if (!resp.ok) {
28
+ const body = await resp.text().catch(() => "");
29
+ throw new Error(`Failed to fetch receipt (${resp.status}): ${body.slice(0, 200)}`);
30
+ }
31
+ return resp.text();
32
+ }
33
+ /**
34
+ * Generate receipt locally without network — used for offline queued DD-RRR-CCC orders.
35
+ */
36
+ export function generateLocalReceipt(order, format = "escpos", logoDataUri) {
37
+ return format === "escpos" ? generateEscpos(order) : generateHtml(order, logoDataUri);
38
+ }
39
+ /**
40
+ * Print an order — tries TCP ESC/POS if a default printer is configured, otherwise
41
+ * generates HTML and uses the PrintFileAdapter (expo-print / Tauri print).
42
+ * `order` must be a full SellerOrder snapshot; for online orders we attempt to
43
+ * fetch the authoritative receipt first so the business logo/address are server-rendered.
44
+ */
45
+ export async function printOrderReceipt(order, opts) {
46
+ const { autoPrint, printers, defaultPrinterId } = getPrinterState();
47
+ const needsTcp = !!defaultPrinterId && autoPrint !== false;
48
+ const printer = needsTcp ? printers.find((p) => p.id === defaultPrinterId) : null;
49
+ const isTcp = !!(printer && printer.type === "tcp");
50
+ // Try authoritative backend receipt first when online (includes logo data URI)
51
+ // For offline/queued orders we generate locally.
52
+ let escpos = null;
53
+ let html = null;
54
+ if (isTcp) {
55
+ try {
56
+ // Prefer backend escpos when we have a real UUID (not DD-RRR-CCC)
57
+ const isLocal = /^[0-9]{2}-[0-9]{3}-[0-9]{3}$/.test(order.order_number) || /^[0-9A-F-]{36}$/.test(order.id) === false && order.id === order.order_number;
58
+ if (!isLocal || order.id) {
59
+ try {
60
+ escpos = await fetchReceipt(order.id, "escpos");
61
+ }
62
+ catch {
63
+ // fall back to local
64
+ }
65
+ }
66
+ if (!escpos)
67
+ escpos = generateLocalReceipt(order, "escpos", opts?.logoDataUri ?? null);
68
+ const tcp = getTcpAdapter();
69
+ if (!tcp)
70
+ throw new Error("TCP adapter not configured");
71
+ await tcp.sendToTcpPrinter(printer.ip, printer.port, escpos);
72
+ return;
73
+ }
74
+ catch (e) {
75
+ console.debug("[print] TCP print failed, falling back to HTML", e);
76
+ }
77
+ }
78
+ // Fallback: HTML via PrintFileAdapter or local share
79
+ try {
80
+ try {
81
+ html = await fetchReceipt(order.id, "html");
82
+ }
83
+ catch {
84
+ // offline
85
+ }
86
+ if (!html)
87
+ html = generateLocalReceipt(order, "html", opts?.logoDataUri ?? null);
88
+ const printAdapter = getPrintFileAdapter();
89
+ if (printAdapter) {
90
+ const { uri } = await printAdapter.printToFileAsync(html);
91
+ if (printAdapter.isSharingAvailable && printAdapter.shareAsync) {
92
+ const available = await printAdapter.isSharingAvailable();
93
+ if (available)
94
+ await printAdapter.shareAsync(uri, { dialogTitle: "Print Receipt", mimeType: "application/pdf" });
95
+ }
96
+ return;
97
+ }
98
+ // No adapter — throw so host can show preview fallback
99
+ throw new Error("PrintFileAdapter not configured — use generateLocalReceipt + WebView preview");
100
+ }
101
+ catch (e) {
102
+ console.debug("[print] HTML print failed", e);
103
+ throw e;
104
+ }
105
+ }
106
+ export async function sendToTcpPrinter(ip, port, text) {
107
+ const tcp = getTcpAdapter();
108
+ if (!tcp)
109
+ throw new Error("TCP socket not available");
110
+ return tcp.sendToTcpPrinter(ip, port, text);
111
+ }
112
+ export const printerService = {
113
+ fetchReceipt,
114
+ generateLocalReceipt,
115
+ printOrderReceipt,
116
+ sendToTcpPrinter,
117
+ generateEscpos,
118
+ generateHtml,
119
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Printer store — local device config, persisted via KV driver.
3
+ * Business app invariant: printers are device-local, never cleared on logout.
4
+ */
5
+ import type { PrinterConfig, PrinterState } from "./types";
6
+ export declare function hydratePrinterStore(): Promise<void>;
7
+ export declare function getPrinterState(): PrinterState;
8
+ export declare function subscribePrinter(listener: () => void): () => void;
9
+ export declare function getPrinterSnapshot(): PrinterState;
10
+ export declare function setDefaultPrinter(id: string | null): void;
11
+ export declare function addPrinter(printer: PrinterConfig): void;
12
+ export declare function updatePrinter(id: string, updates: Partial<PrinterConfig>): void;
13
+ export declare function removePrinter(id: string): void;
14
+ export declare function setAutoPrint(enabled: boolean): void;
15
+ export declare const printerStore: {
16
+ getState: typeof getPrinterState;
17
+ subscribe: typeof subscribePrinter;
18
+ hydrate: typeof hydratePrinterStore;
19
+ setDefaultPrinter: typeof setDefaultPrinter;
20
+ addPrinter: typeof addPrinter;
21
+ updatePrinter: typeof updatePrinter;
22
+ removePrinter: typeof removePrinter;
23
+ setAutoPrint: typeof setAutoPrint;
24
+ };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Printer store — local device config, persisted via KV driver.
3
+ * Business app invariant: printers are device-local, never cleared on logout.
4
+ */
5
+ import { getKVDriver } from "../adapters/kv";
6
+ import { STORAGE_KEYS } from "../utils/constants";
7
+ const listeners = new Set();
8
+ let state = {
9
+ defaultPrinterId: null,
10
+ printers: [],
11
+ autoPrint: false,
12
+ };
13
+ let hydrated = false;
14
+ function notify() {
15
+ listeners.forEach((l) => l());
16
+ }
17
+ async function persist() {
18
+ try {
19
+ const kv = getKVDriver();
20
+ await kv.setItem(STORAGE_KEYS.PRINTER_SETTINGS, JSON.stringify(state));
21
+ }
22
+ catch { }
23
+ }
24
+ export async function hydratePrinterStore() {
25
+ if (hydrated)
26
+ return;
27
+ hydrated = true;
28
+ try {
29
+ const kv = getKVDriver();
30
+ const raw = await kv.getItem(STORAGE_KEYS.PRINTER_SETTINGS);
31
+ if (raw) {
32
+ const parsed = JSON.parse(raw);
33
+ state = {
34
+ defaultPrinterId: parsed.defaultPrinterId ?? null,
35
+ printers: Array.isArray(parsed.printers) ? parsed.printers : [],
36
+ autoPrint: !!parsed.autoPrint,
37
+ };
38
+ notify();
39
+ }
40
+ }
41
+ catch { }
42
+ }
43
+ export function getPrinterState() {
44
+ return state;
45
+ }
46
+ export function subscribePrinter(listener) {
47
+ listeners.add(listener);
48
+ return () => listeners.delete(listener);
49
+ }
50
+ export function getPrinterSnapshot() {
51
+ return state;
52
+ }
53
+ export function setDefaultPrinter(id) {
54
+ state = { ...state, defaultPrinterId: id };
55
+ notify();
56
+ void persist();
57
+ }
58
+ export function addPrinter(printer) {
59
+ state = { ...state, printers: [...state.printers, printer] };
60
+ notify();
61
+ void persist();
62
+ }
63
+ export function updatePrinter(id, updates) {
64
+ state = {
65
+ ...state,
66
+ printers: state.printers.map((p) => (p.id === id ? { ...p, ...updates } : p)),
67
+ };
68
+ notify();
69
+ void persist();
70
+ }
71
+ export function removePrinter(id) {
72
+ state = {
73
+ ...state,
74
+ printers: state.printers.filter((p) => p.id !== id),
75
+ defaultPrinterId: state.defaultPrinterId === id ? null : state.defaultPrinterId,
76
+ };
77
+ notify();
78
+ void persist();
79
+ }
80
+ export function setAutoPrint(enabled) {
81
+ state = { ...state, autoPrint: enabled };
82
+ notify();
83
+ void persist();
84
+ }
85
+ export const printerStore = {
86
+ getState: getPrinterState,
87
+ subscribe: subscribePrinter,
88
+ hydrate: hydratePrinterStore,
89
+ setDefaultPrinter,
90
+ addPrinter,
91
+ updatePrinter,
92
+ removePrinter,
93
+ setAutoPrint,
94
+ };
@@ -0,0 +1,12 @@
1
+ export interface PrinterConfig {
2
+ id: string;
3
+ name: string;
4
+ ip: string;
5
+ port: number;
6
+ type: "tcp" | "bluetooth";
7
+ }
8
+ export interface PrinterState {
9
+ defaultPrinterId: string | null;
10
+ printers: PrinterConfig[];
11
+ autoPrint: boolean;
12
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teincfood/core",
3
- "version": "0.7.7",
3
+ "version": "0.7.8",
4
4
  "description": "TeincFood shared offline-first core — types, sync engine, local DB, reference data, and repositories for mobile + desktop",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",