@teincfood/core 0.9.4 → 0.9.5
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.
- package/dist/printer/receipt.d.ts +1 -0
- package/dist/printer/receipt.js +85 -27
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Keep this file in sync with backend when template changes.
|
|
8
8
|
*/
|
|
9
9
|
import type { SellerOrder } from "../types/api/seller.api";
|
|
10
|
+
export declare function escposText(s: string | null | undefined): string;
|
|
10
11
|
export declare function calloutNumber(order: SellerOrder): string;
|
|
11
12
|
export declare function generateEscpos(order: SellerOrder): string;
|
|
12
13
|
export declare function generateHtmlAsync(order: SellerOrder, opts?: {
|
package/dist/printer/receipt.js
CHANGED
|
@@ -18,9 +18,43 @@ const ESC_BOLD_OFF = "\x1B\x45\x00";
|
|
|
18
18
|
const GS_SIZE_NORMAL = "\x1D\x21\x00";
|
|
19
19
|
const GS_SIZE_DOUBLE_W = "\x1D\x21\x11";
|
|
20
20
|
const GS_SIZE_DOUBLE_WH = "\x1D\x21\x33";
|
|
21
|
+
const GS_BAR_HEIGHT = "\x1D\x68\x3C";
|
|
22
|
+
const GS_BAR_WIDTH = "\x1D\x77\x02";
|
|
21
23
|
const LF = "\x0A";
|
|
22
24
|
const ESC_FEED_3 = "\x1B\x64\x03";
|
|
23
25
|
const GS_CUT_FULL = "\x1D\x56\x00";
|
|
26
|
+
/**
|
|
27
|
+
* Thermal printers speak bytes, not Unicode. Control sequences are pushed
|
|
28
|
+
* raw, but every human-readable text segment must pass through here first:
|
|
29
|
+
* known symbols map to ASCII, anything else non-ASCII becomes "?".
|
|
30
|
+
* Without this, ₵/═/─/accents print as gibberish (UTF-8 bytes read as CP437).
|
|
31
|
+
*/
|
|
32
|
+
const ESCPOS_REPLACEMENTS = [
|
|
33
|
+
[/[₵]/g, "GHc"],
|
|
34
|
+
[/[€]/g, "EUR"],
|
|
35
|
+
[/[£]/g, "GBP"],
|
|
36
|
+
[/[—–]/g, "-"],
|
|
37
|
+
[/[•·]/g, "*"],
|
|
38
|
+
[/[═╬]/g, "="],
|
|
39
|
+
[/[─│]/g, "-"],
|
|
40
|
+
[/[“”]/g, '"'],
|
|
41
|
+
[/[‘’`]/g, "'"],
|
|
42
|
+
[/[…]/g, "..."],
|
|
43
|
+
[/[×]/g, "x"],
|
|
44
|
+
[/[°]/g, "deg"],
|
|
45
|
+
[/ /g, " "],
|
|
46
|
+
];
|
|
47
|
+
export function escposText(s) {
|
|
48
|
+
if (s == null)
|
|
49
|
+
return "";
|
|
50
|
+
let out = String(s);
|
|
51
|
+
for (const [re, sub] of ESCPOS_REPLACEMENTS)
|
|
52
|
+
out = out.replace(re, sub);
|
|
53
|
+
// Strip any remaining non-ASCII (accents, emoji, CJK) — the printer cannot
|
|
54
|
+
// render them and would print garbage bytes.
|
|
55
|
+
out = out.replace(/[^\x20-\x7E\n]/g, "?");
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
24
58
|
function padR(text, width) {
|
|
25
59
|
if (text.length >= width)
|
|
26
60
|
return text.slice(0, width);
|
|
@@ -60,14 +94,19 @@ function formatDatetime(iso) {
|
|
|
60
94
|
}
|
|
61
95
|
}
|
|
62
96
|
export function calloutNumber(order) {
|
|
97
|
+
// The backend assigns a real callout_number — always prefer it.
|
|
98
|
+
const direct = order.callout_number;
|
|
99
|
+
if (typeof direct === "number" && Number.isFinite(direct))
|
|
100
|
+
return String(Math.abs(direct));
|
|
63
101
|
const num = order.order_number || "";
|
|
64
102
|
if (!num)
|
|
65
103
|
return "----";
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
104
|
+
// Strip non-digits first: local DD-RRR-CCC numbers end in "-CCC" and
|
|
105
|
+
// parseInt("-005") would give -5 (the old "-5" bug).
|
|
106
|
+
const digits = num.slice(-4).replace(/\D/g, "");
|
|
107
|
+
if (!digits)
|
|
108
|
+
return num.slice(-4);
|
|
109
|
+
return String(parseInt(digits, 10));
|
|
71
110
|
}
|
|
72
111
|
function itemName(item) {
|
|
73
112
|
return item.menu_item?.name || "(deleted item)";
|
|
@@ -115,8 +154,19 @@ function businessAddress(business) {
|
|
|
115
154
|
return null;
|
|
116
155
|
if (b.location?.address)
|
|
117
156
|
return b.location.address;
|
|
118
|
-
if (b.address)
|
|
157
|
+
if (typeof b.address === "string")
|
|
119
158
|
return b.address;
|
|
159
|
+
// Desktop snapshots carry a structured address object — flatten it
|
|
160
|
+
// (printing the object itself would render "[object Object]").
|
|
161
|
+
if (b.address && typeof b.address === "object") {
|
|
162
|
+
if (b.address.fullAddress)
|
|
163
|
+
return b.address.fullAddress;
|
|
164
|
+
if (b.address.full_address)
|
|
165
|
+
return b.address.full_address;
|
|
166
|
+
const parts = [b.address.street, b.address.city].filter(Boolean);
|
|
167
|
+
if (parts.length)
|
|
168
|
+
return parts.join(", ");
|
|
169
|
+
}
|
|
120
170
|
return null;
|
|
121
171
|
}
|
|
122
172
|
function businessLogoUrl(order) {
|
|
@@ -165,39 +215,47 @@ export function generateEscpos(order) {
|
|
|
165
215
|
const parts = [];
|
|
166
216
|
const push = (s) => { if (s != null)
|
|
167
217
|
parts.push(s); };
|
|
218
|
+
// Sanitized text push: every human-readable segment goes through escposText
|
|
219
|
+
// (control codes and barcode data use raw push).
|
|
220
|
+
const pushT = (s) => { if (s != null)
|
|
221
|
+
parts.push(escposText(s)); };
|
|
168
222
|
// Header — matches receipt.ex to_escpos/1
|
|
223
|
+
// NOTE: no logo in ESC/POS text mode — a logo URL cannot be rendered without
|
|
224
|
+
// a raster pipeline (GS v 0). HTML receipts embed the logo instead.
|
|
169
225
|
push(ESC_INIT);
|
|
170
226
|
push(ESC_ALIGN_CENTER);
|
|
171
227
|
push(GS_SIZE_DOUBLE_WH);
|
|
172
|
-
|
|
228
|
+
pushT(business?.name || "Teinc Food");
|
|
173
229
|
push(LF);
|
|
174
230
|
push(GS_SIZE_NORMAL);
|
|
175
231
|
const addr = businessAddress(business);
|
|
176
232
|
if (addr)
|
|
177
|
-
|
|
233
|
+
pushT(addr + LF);
|
|
178
234
|
const bPhone = business?.phone;
|
|
179
235
|
if (bPhone)
|
|
180
|
-
|
|
236
|
+
pushT(`Tel: ${bPhone}${LF}`);
|
|
181
237
|
push(GS_SIZE_NORMAL);
|
|
182
|
-
|
|
238
|
+
pushT(doubleLineEscpos());
|
|
183
239
|
// Order info
|
|
184
240
|
push(ESC_ALIGN_LEFT);
|
|
185
|
-
|
|
241
|
+
pushT(`Order: ${order.order_number}${LF}`);
|
|
186
242
|
// callout_box_escpos
|
|
187
243
|
push(ESC_ALIGN_CENTER);
|
|
188
244
|
push(GS_SIZE_DOUBLE_W);
|
|
189
|
-
|
|
245
|
+
pushT(`Callout: ${calloutNumber(order)}${LF}`);
|
|
190
246
|
push(GS_SIZE_NORMAL);
|
|
191
247
|
push(LF);
|
|
192
248
|
push(ESC_ALIGN_LEFT);
|
|
193
|
-
|
|
194
|
-
|
|
249
|
+
pushT(`Date: ${formatDatetime(order.inserted_at)}${LF}`);
|
|
250
|
+
pushT(`Type: ${orderTypeEscpos(order)}${LF}`);
|
|
195
251
|
const custLine = customerLineEscpos(order);
|
|
196
252
|
if (custLine)
|
|
197
|
-
|
|
253
|
+
pushT(custLine);
|
|
198
254
|
push(LF);
|
|
199
|
-
// Barcode — CODE128 GS k 73
|
|
255
|
+
// Barcode — CODE128 GS k 73, sized down (default height is huge).
|
|
200
256
|
push(ESC_ALIGN_CENTER);
|
|
257
|
+
push(GS_BAR_HEIGHT);
|
|
258
|
+
push(GS_BAR_WIDTH);
|
|
201
259
|
const data = order.order_number || "";
|
|
202
260
|
push(String.fromCharCode(0x1d, 0x6b, 0x49, data.length) + data);
|
|
203
261
|
push(LF);
|
|
@@ -205,44 +263,44 @@ export function generateEscpos(order) {
|
|
|
205
263
|
push(ESC_ALIGN_LEFT);
|
|
206
264
|
push(`${padR("Item", 16)} Qty Price`);
|
|
207
265
|
push(LF);
|
|
208
|
-
|
|
266
|
+
pushT(singleLineEscpos());
|
|
209
267
|
push(LF);
|
|
210
268
|
for (const item of items) {
|
|
211
269
|
const qty = item.quantity || 1;
|
|
212
270
|
const price = formatMoney((item.unit_price ?? 0) * qty, order);
|
|
213
|
-
|
|
271
|
+
pushT(` ${padR(itemName(item), 16)} ${padR(String(qty), 4)} ${padR(price, 9)}${LF}`);
|
|
214
272
|
const addons = item.order_item_addons ?? item.addons ?? [];
|
|
215
273
|
for (const addon of addons) {
|
|
216
274
|
const aName = addonName(addon);
|
|
217
275
|
const aPriceMinor = addon.price_minor ?? addon.price ?? 0;
|
|
218
276
|
const addonPrice = formatMoney(aPriceMinor, order);
|
|
219
|
-
|
|
277
|
+
pushT(` + ${padR(aName, 12)} ${padR(addonPrice, 9)}${LF}`);
|
|
220
278
|
}
|
|
221
279
|
}
|
|
222
|
-
|
|
280
|
+
pushT(singleLineEscpos());
|
|
223
281
|
push(LF);
|
|
224
282
|
// Subtotal section — mirrors subtotal_section_escpos/2
|
|
225
|
-
|
|
283
|
+
pushT(`${padR("Subtotal:", 20)} ${formatMoney(order.subtotal_minor, order)}${LF}`);
|
|
226
284
|
if (order.delivery_fee_minor && order.delivery_fee_minor > 0) {
|
|
227
|
-
|
|
285
|
+
pushT(`${padR("Delivery Fee:", 20)} ${formatMoney(order.delivery_fee_minor, order)}${LF}`);
|
|
228
286
|
}
|
|
229
287
|
for (const tax of taxLines) {
|
|
230
288
|
const rateStr = tax.rate_basis_points ? `(${Math.floor(tax.rate_basis_points / 100)}%)` : "";
|
|
231
289
|
const label = `${tax.name || ""} ${rateStr}`.trim().slice(0, 19);
|
|
232
|
-
|
|
290
|
+
pushT(`${padR(label, 20)} ${formatMoney(tax.tax_amount_minor, order)}${LF}`);
|
|
233
291
|
}
|
|
234
292
|
if (order.discount_minor && order.discount_minor > 0) {
|
|
235
|
-
|
|
293
|
+
pushT(`${padR("Discount:", 20)} -${formatMoney(order.discount_minor, order)}${LF}`);
|
|
236
294
|
}
|
|
237
295
|
push(ESC_BOLD_ON);
|
|
238
|
-
|
|
296
|
+
pushT(`${padR("TOTAL:", 20)} ${formatMoney(order.total_minor, order)}${LF}`);
|
|
239
297
|
push(ESC_BOLD_OFF);
|
|
240
298
|
push(LF);
|
|
241
299
|
push(ESC_ALIGN_CENTER);
|
|
242
|
-
|
|
300
|
+
pushT(paymentLineEscpos(order));
|
|
243
301
|
push(LF);
|
|
244
302
|
// Footer
|
|
245
|
-
|
|
303
|
+
pushT(doubleLineEscpos());
|
|
246
304
|
push("Thank you for your order!" + LF);
|
|
247
305
|
push("Come back soon!" + LF);
|
|
248
306
|
push(ESC_FEED_3);
|
package/package.json
CHANGED