@alizadefaig/pos-server 1.2.25 → 1.2.27

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.
Files changed (116) hide show
  1. package/app/core/Context.js +170 -0
  2. package/app/core/auth/index.js +133 -0
  3. package/app/core/auth/license.js +45 -0
  4. package/app/core/auth/posMode.js +260 -0
  5. package/app/core/auth/sanctum.js +209 -0
  6. package/app/core/auth/scope.js +231 -0
  7. package/app/core/emitter.js +67 -0
  8. package/app/core/errors.js +18 -0
  9. package/app/core/guard.js +54 -0
  10. package/app/core/idempotency.js +196 -0
  11. package/app/core/index.js +374 -0
  12. package/app/core/models/Order.js +176 -0
  13. package/app/core/models/OrderProduct.js +249 -0
  14. package/app/core/repositories/EventRepository.js +51 -0
  15. package/app/core/repositories/OrderProductRepository.js +225 -0
  16. package/app/core/repositories/OrderRepository.js +274 -0
  17. package/app/core/repositories/OutboxRepository.js +52 -0
  18. package/app/core/repositories/PaymentRepository.js +43 -0
  19. package/app/core/repositories/PrinterRepository.js +179 -0
  20. package/app/core/repositories/ProductRelations.js +148 -0
  21. package/app/core/repositories/SnapshotRepository.js +197 -0
  22. package/app/core/resources/HallResource.js +54 -0
  23. package/app/core/resources/OrderResource.js +177 -0
  24. package/app/core/resources/ProductResource.js +186 -0
  25. package/app/core/resources/ShiftResource.js +42 -0
  26. package/app/core/resources/TeamResource.js +85 -0
  27. package/app/core/resources/casts.js +55 -0
  28. package/app/core/services/AuthService.js +468 -0
  29. package/app/core/services/InitService.js +277 -0
  30. package/app/core/services/KdsService.js +639 -0
  31. package/app/core/services/OrderProductService.js +309 -0
  32. package/app/core/services/OrderService.js +903 -0
  33. package/app/core/services/PrintService.js +715 -0
  34. package/app/core/services/PrinterAdminService.js +324 -0
  35. package/app/core/services/ReportService.js +581 -0
  36. package/app/core/services/ShiftService.js +199 -0
  37. package/app/core/services/StorageService.js +425 -0
  38. package/app/core/services/TelegramService.js +162 -0
  39. package/app/core/support/db.js +48 -0
  40. package/app/core/support/ids.js +9 -0
  41. package/app/core/support/imageData.js +135 -0
  42. package/app/core/support/images.js +92 -0
  43. package/app/core/support/lanHandoff.js +105 -0
  44. package/app/core/support/lang.js +98 -0
  45. package/app/core/support/lock.js +63 -0
  46. package/app/core/support/num.js +51 -0
  47. package/app/core/support/php.js +125 -0
  48. package/app/core/support/pin.js +34 -0
  49. package/app/core/support/receipts.js +278 -0
  50. package/app/core/support/rows.js +38 -0
  51. package/app/core/support/str.js +31 -0
  52. package/app/core/support/teamSettings.js +77 -0
  53. package/app/core/support/telegram.js +253 -0
  54. package/app/core/support/telegramReport.js +143 -0
  55. package/app/core/support/time.js +58 -0
  56. package/app/core/support/tz.js +80 -0
  57. package/app/core/support/unlock.js +113 -0
  58. package/app/core/timers.js +151 -0
  59. package/app/db/Database.js +197 -1
  60. package/app/db/Migrator.js +45 -0
  61. package/app/db/driver.js +248 -0
  62. package/app/db/migrations/001_snapshot.sql +214 -0
  63. package/app/db/migrations/002_orders.sql +190 -0
  64. package/app/db/migrations/003_idempotency.sql +18 -0
  65. package/app/db/migrations/004_shifts.sql +21 -0
  66. package/app/db/migrations/005_printers.sql +43 -0
  67. package/app/db/migrations/006_sync.sql +136 -0
  68. package/app/db/migrations/007_printer_mac.sql +4 -0
  69. package/app/db/migrations/008_kds.sql +94 -0
  70. package/app/db/migrations/009_product_categories.sql +20 -0
  71. package/app/db/migrations/010_printers_sync.sql +43 -0
  72. package/app/db/migrations/011_team_logos.sql +13 -0
  73. package/app/db/migrations/012_category_workshop.sql +4 -0
  74. package/app/monitor.js +185 -0
  75. package/app/printers/address.js +348 -0
  76. package/app/proxy.js +20 -3
  77. package/app/relay/masters.js +173 -0
  78. package/app/security.js +218 -0
  79. package/app/socket/auth.js +160 -0
  80. package/app/socket/request.js +87 -0
  81. package/app/socket/server.js +121 -19
  82. package/app/sync/bridge.js +162 -0
  83. package/app/sync/cloud.js +120 -0
  84. package/app/sync/config.js +185 -0
  85. package/app/sync/discovery.js +546 -0
  86. package/app/sync/index.js +1644 -0
  87. package/app/sync/kv.js +26 -0
  88. package/app/sync/lan.js +139 -0
  89. package/app/sync/lanDevices.js +177 -0
  90. package/app/sync/lanFront.js +431 -0
  91. package/app/sync/mirror.js +236 -0
  92. package/app/sync/push.js +324 -0
  93. package/app/sync/reconcile.js +161 -0
  94. package/app/sync/signal.js +202 -0
  95. package/app/sync/snapshot.js +189 -0
  96. package/app/sync/store.js +234 -0
  97. package/app/tcp.js +11 -2
  98. package/package.json +6 -3
  99. package/routes/common/api/auth.js +38 -0
  100. package/routes/common/api/kds.js +37 -0
  101. package/routes/common/api/orderProducts.js +81 -0
  102. package/routes/common/api/orders.js +176 -0
  103. package/routes/common/api/printersCore.js +35 -0
  104. package/routes/common/api/reports.js +29 -0
  105. package/routes/common/socket.js +15 -35
  106. package/routes/common/tax.js +35 -7
  107. package/routes/desktop/api/offline.js +146 -0
  108. package/routes/desktop/api/printers.js +288 -5
  109. package/routes/desktop/local.js +130 -0
  110. package/routes/server/api/login.js +37 -0
  111. package/routes/server/api/platform.js +31 -0
  112. package/routes/server/api/shifts.js +25 -0
  113. package/routes/server/master.js +24 -0
  114. package/server.js +111 -4
  115. package/utils.js +6 -0
  116. package/tests/usbPrinterTest.js +0 -20
@@ -0,0 +1,277 @@
1
+ const {teamResource, terminalResource, teamUserResource} = require('../resources/TeamResource');
2
+ const {hallResource} = require('../resources/HallResource');
3
+ const {productResource, categoryResource, extraProductResource} = require('../resources/ProductResource');
4
+ const {nullableBool, modelArray, takeText, jsonText} = require('../resources/casts');
5
+ const {transArray, FALLBACK_LOCALE} = require('../support/lang');
6
+ const {imageUrls} = require('../support/images');
7
+ const {unique, groupBy, indexBy, alive, whereIn} = require('../support/rows');
8
+ const ProductRelations = require('../repositories/ProductRelations');
9
+ const posMode = require('../auth/posMode');
10
+ const lanHandoff = require('../support/lanHandoff');
11
+ const pin = require('../support/pin');
12
+ const {HttpError} = require('../errors');
13
+
14
+ /**
15
+ * Облачные справочники POS: порт AuthController::init и соседних чтений (ProductService,
16
+ * HallService, UserService, CategoryService, getExtraProducts). Смены — ShiftService.
17
+ *
18
+ * Eloquent грузит связи отдельными запросами `WHERE … IN (…)` — здесь то же самое, без N+1:
19
+ * мутатор цеха товара в PHP читает категорию по одной на товар, здесь — одним запросом.
20
+ *
21
+ * Схема — Laravel (Postgres). Мягко удалённые строки отсекаются только под Postgres: в локальном
22
+ * снапшоте колонки deleted_at у части таблиц нет, как и удалённых строк.
23
+ */
24
+
25
+ const TOP_PRODUCT_TYPES = ['dish', 'product', 'timer'];
26
+
27
+ class InitService {
28
+ /**
29
+ * @param {{db: object, context: import('../Context'), orders: object}} services
30
+ */
31
+ constructor({db, context, orders}) {
32
+ this.db = db;
33
+ this.context = context;
34
+ this.orders = orders;
35
+ this.pgsql = db.dialect === 'pgsql';
36
+ }
37
+
38
+ alive(alias = null) {
39
+ return alive(this.db, alias);
40
+ }
41
+
42
+ /** Локаль ответа — как у PlatformApiMiddleware: язык команды, иначе app.locale. */
43
+ get locale() {
44
+ return this.context.localeCode || process.env.APP_LOCALE || FALLBACK_LOCALE;
45
+ }
46
+
47
+ whereIn(sql, ids, params = []) {
48
+ return whereIn(this.db, sql, ids, params);
49
+ }
50
+
51
+ // ------------------------------------------------------------ app.init
52
+
53
+ async init() {
54
+ await this.context.load();
55
+ const team = this.context.getTeam();
56
+ const terminal = this.context.getTerminal();
57
+ if (!team || !terminal) {
58
+ // В PHP getTerminal()->load(…) на null — Error и 500.
59
+ throw new HttpError(500, {message: 'Server Error'});
60
+ }
61
+ const teamId = this.context.teamId;
62
+ const companyId = this.context.companyId;
63
+
64
+ const [header, halls, products, users, orders, categories, units, workshops, paymentMethods, orderSources] = await Promise.all([
65
+ this.header(team, terminal),
66
+ this.halls(companyId),
67
+ this.products(teamId),
68
+ this.users(teamId, team),
69
+ this.orders.getOpenOrders(companyId),
70
+ this.categories(teamId),
71
+ this.units(),
72
+ this.workshops(teamId),
73
+ this.paymentMethods(teamId),
74
+ this.orderSources(teamId),
75
+ ]);
76
+
77
+ const result = {
78
+ team: header.team,
79
+ terminal: header.terminal,
80
+ halls,
81
+ products,
82
+ users,
83
+ orders,
84
+ categories,
85
+ units,
86
+ workshops,
87
+ payment_methods: paymentMethods,
88
+ order_sources: orderSources,
89
+ };
90
+ if (pin.hashed()) {
91
+ result.pin_salt = pin.salt(teamId);
92
+ }
93
+ if (lanHandoff.enabled()) {
94
+ result.lan_master = await lanHandoff.masterFor(this.db, companyId);
95
+ }
96
+ return result;
97
+ }
98
+
99
+ /** TeamResource + TerminalResource (с company и workshops). */
100
+ async header(team, terminal) {
101
+ const [currency, language, company, terminalWorkshops, urls, mode] = await Promise.all([
102
+ team.currency_id
103
+ ? this.db.get(`SELECT symbol FROM currencies WHERE id = ?${this.alive()}`, [team.currency_id])
104
+ : null,
105
+ team.language_id
106
+ ? this.db.get(`SELECT code FROM languages WHERE id = ?${this.alive()}`, [team.language_id])
107
+ : null,
108
+ terminal.company_id
109
+ ? this.db.get(`SELECT name FROM companies WHERE id = ?${this.alive()}`, [terminal.company_id])
110
+ : null,
111
+ this.db.all(
112
+ `SELECT pw.id, pw.name FROM product_workshops pw
113
+ INNER JOIN terminal_workshop tw ON pw.id = tw.product_workshop_id
114
+ WHERE tw.terminal_id = ?${this.alive('pw')}`,
115
+ [terminal.id]
116
+ ),
117
+ imageUrls(this.db, [team.logo]),
118
+ posMode.state(this.db, terminal.company_id),
119
+ ]);
120
+ const currencySymbol = currency ? currency.symbol : null;
121
+ return {
122
+ team: teamResource(team, {
123
+ currencySymbol,
124
+ languageCode: language ? language.code : null,
125
+ urls,
126
+ locale: this.locale,
127
+ }),
128
+ terminal: terminalResource(terminal, {
129
+ companyName: company ? company.name : null,
130
+ workshops: terminalWorkshops,
131
+ posMode: mode,
132
+ }),
133
+ };
134
+ }
135
+
136
+ // ------------------------------------------------------------ залы
137
+
138
+ /** HallService::getHalls — залы по order, столы по order. */
139
+ async halls(companyId) {
140
+ const halls = await this.db.all(
141
+ `SELECT * FROM halls WHERE company_id = ?${this.alive()} ORDER BY "order"`,
142
+ [companyId]
143
+ );
144
+ if (!halls.length) {
145
+ return [];
146
+ }
147
+ const tables = await this.whereIn(
148
+ `SELECT * FROM tables WHERE hall_id IN (:ids)${this.alive()} ORDER BY "order"`,
149
+ unique(halls.map((hall) => hall.id))
150
+ );
151
+ const byHall = groupBy(tables, 'hall_id');
152
+ return halls.map((hall) => hallResource(hall, byHall.get(String(hall.id)) || []));
153
+ }
154
+
155
+ // ------------------------------------------------------------ товары
156
+
157
+ /** ProductService::allProducts — верхние товары со связями. */
158
+ async products(teamId) {
159
+ const products = await this.db.all(
160
+ `SELECT * FROM products
161
+ WHERE parent_id IS NULL AND team_id = ? AND type IN (${TOP_PRODUCT_TYPES.map(() => '?').join(', ')})${this.alive()}
162
+ ORDER BY "order"`,
163
+ [teamId, ...TOP_PRODUCT_TYPES]
164
+ );
165
+ if (!products.length) {
166
+ return [];
167
+ }
168
+ const rel = await new ProductRelations(this.db, this.locale).load(products);
169
+ return products.map((product) => productResource(product, rel));
170
+ }
171
+
172
+ /** CategoryService::all. */
173
+ async categories(teamId) {
174
+ const rows = await this.db.all(
175
+ `SELECT * FROM product_categories WHERE team_id = ?${this.alive()} ORDER BY "order"`,
176
+ [teamId]
177
+ );
178
+ const urls = await imageUrls(this.db, rows.map((row) => row.image));
179
+ return rows.map((row) => categoryResource(row, {urls, locale: this.locale}));
180
+ }
181
+
182
+ /** `products.getExtraProducts` / `orderProducts.getExtraProducts`. */
183
+ async extraProducts(teamId) {
184
+ const rows = await this.db.all(
185
+ `SELECT id, full_name, product_workshop_id, price FROM products
186
+ WHERE type = ? AND team_id = ?${this.alive()}`,
187
+ ['extra', teamId]
188
+ );
189
+ return rows.map((row) => extraProductResource(row, this.locale));
190
+ }
191
+
192
+ // ------------------------------------------------------------ сотрудники
193
+
194
+ /**
195
+ * UserService::all — `latest()`. Смены в коллекции всегда null: TeamUserResource берёт их из
196
+ * своих свойств конструктора, а коллекция создаёт ресурс без них (eager-load workShift
197
+ * в ответ не попадает).
198
+ */
199
+ async users(teamId, team) {
200
+ const members = await this.db.all(
201
+ `SELECT * FROM team_users WHERE team_id = ?${this.alive()} ORDER BY created_at DESC`,
202
+ [teamId]
203
+ );
204
+ if (!members.length) {
205
+ return [];
206
+ }
207
+ const userIds = unique(members.map((member) => member.user_id));
208
+ const roleIds = unique(members.map((member) => member.role_id));
209
+ const [users, roles, events] = await Promise.all([
210
+ userIds.length ? this.whereIn(`SELECT id, name FROM users WHERE id IN (:ids)${this.alive()}`, userIds) : [],
211
+ roleIds.length ? this.whereIn(`SELECT * FROM roles WHERE id IN (:ids)${this.alive()}`, roleIds) : [],
212
+ roleIds.length ? this.whereIn(
213
+ `SELECT pe."group", pe.slug, per.can, per.role_id
214
+ FROM platform_events pe
215
+ INNER JOIN platform_event_roles per ON pe.id = per.platform_event_id
216
+ WHERE per.role_id IN (:ids)${this.alive('pe')}`,
217
+ roleIds
218
+ ) : [],
219
+ ]);
220
+ const usersById = indexBy(users);
221
+ const rolesById = indexBy(roles);
222
+ const scopesByRole = new Map();
223
+ for (const event of events) {
224
+ const key = String(event.role_id);
225
+ if (!scopesByRole.has(key)) {
226
+ scopesByRole.set(key, {});
227
+ }
228
+ scopesByRole.get(key)[`${event.group}:${event.slug}`] = event.can;
229
+ }
230
+ return members.map((member) => teamUserResource(member, {
231
+ user: usersById.get(String(member.user_id)) || null,
232
+ role: rolesById.get(String(member.role_id)) || null,
233
+ scopes: scopesByRole.get(String(member.role_id)) || {},
234
+ teamOwnerId: team.user_id,
235
+ teamId,
236
+ }));
237
+ }
238
+
239
+ // ------------------------------------------------------------ справочники
240
+
241
+ /** `Unit::query()->get(['id', 'code', 'name', 'short_name'])` — без фильтра команды. */
242
+ async units() {
243
+ const rows = await this.db.all(`SELECT id, code, name, short_name FROM units WHERE 1 = 1${this.alive()}`);
244
+ return rows.map((row) => ({
245
+ id: row.id,
246
+ code: row.code,
247
+ name: transArray(row.name, this.locale),
248
+ short_name: transArray(row.short_name, this.locale),
249
+ }));
250
+ }
251
+
252
+ async workshops(teamId) {
253
+ const rows = await this.db.all(
254
+ `SELECT *${jsonText(this.db, ['config'])} FROM product_workshops WHERE team_id = ?${this.alive()}`,
255
+ [teamId]
256
+ );
257
+ return rows.map((row) => modelArray(takeText(row, ['config']), {print_ticket: nullableBool}));
258
+ }
259
+
260
+ async paymentMethods(teamId) {
261
+ const rows = await this.db.all(
262
+ `SELECT *${jsonText(this.db, ['data'])} FROM payment_methods WHERE team_id = ?${this.alive()}`,
263
+ [teamId]
264
+ );
265
+ return rows.map((row) => modelArray(takeText(row, ['data'])));
266
+ }
267
+
268
+ async orderSources(teamId) {
269
+ const rows = await this.db.all(
270
+ `SELECT * FROM order_sources WHERE team_id = ? AND show_in_terminal = ?${this.alive()}`,
271
+ [teamId, true]
272
+ );
273
+ return rows.map((row) => modelArray(row, {show_in_terminal: nullableBool}));
274
+ }
275
+ }
276
+
277
+ module.exports = InitService;