@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.
- package/app/core/Context.js +170 -0
- package/app/core/auth/index.js +133 -0
- package/app/core/auth/license.js +45 -0
- package/app/core/auth/posMode.js +260 -0
- package/app/core/auth/sanctum.js +209 -0
- package/app/core/auth/scope.js +231 -0
- package/app/core/emitter.js +67 -0
- package/app/core/errors.js +18 -0
- package/app/core/guard.js +54 -0
- package/app/core/idempotency.js +196 -0
- package/app/core/index.js +374 -0
- package/app/core/models/Order.js +176 -0
- package/app/core/models/OrderProduct.js +249 -0
- package/app/core/repositories/EventRepository.js +51 -0
- package/app/core/repositories/OrderProductRepository.js +225 -0
- package/app/core/repositories/OrderRepository.js +274 -0
- package/app/core/repositories/OutboxRepository.js +52 -0
- package/app/core/repositories/PaymentRepository.js +43 -0
- package/app/core/repositories/PrinterRepository.js +179 -0
- package/app/core/repositories/ProductRelations.js +148 -0
- package/app/core/repositories/SnapshotRepository.js +197 -0
- package/app/core/resources/HallResource.js +54 -0
- package/app/core/resources/OrderResource.js +177 -0
- package/app/core/resources/ProductResource.js +186 -0
- package/app/core/resources/ShiftResource.js +42 -0
- package/app/core/resources/TeamResource.js +85 -0
- package/app/core/resources/casts.js +55 -0
- package/app/core/services/AuthService.js +468 -0
- package/app/core/services/InitService.js +277 -0
- package/app/core/services/KdsService.js +639 -0
- package/app/core/services/OrderProductService.js +309 -0
- package/app/core/services/OrderService.js +903 -0
- package/app/core/services/PrintService.js +715 -0
- package/app/core/services/PrinterAdminService.js +324 -0
- package/app/core/services/ReportService.js +581 -0
- package/app/core/services/ShiftService.js +199 -0
- package/app/core/services/StorageService.js +425 -0
- package/app/core/services/TelegramService.js +162 -0
- package/app/core/support/db.js +48 -0
- package/app/core/support/ids.js +9 -0
- package/app/core/support/imageData.js +135 -0
- package/app/core/support/images.js +92 -0
- package/app/core/support/lanHandoff.js +105 -0
- package/app/core/support/lang.js +98 -0
- package/app/core/support/lock.js +63 -0
- package/app/core/support/num.js +51 -0
- package/app/core/support/php.js +125 -0
- package/app/core/support/pin.js +34 -0
- package/app/core/support/receipts.js +278 -0
- package/app/core/support/rows.js +38 -0
- package/app/core/support/str.js +31 -0
- package/app/core/support/teamSettings.js +77 -0
- package/app/core/support/telegram.js +253 -0
- package/app/core/support/telegramReport.js +143 -0
- package/app/core/support/time.js +58 -0
- package/app/core/support/tz.js +80 -0
- package/app/core/support/unlock.js +113 -0
- package/app/core/timers.js +151 -0
- package/app/db/Database.js +197 -1
- package/app/db/Migrator.js +45 -0
- package/app/db/driver.js +248 -0
- package/app/db/migrations/001_snapshot.sql +214 -0
- package/app/db/migrations/002_orders.sql +190 -0
- package/app/db/migrations/003_idempotency.sql +18 -0
- package/app/db/migrations/004_shifts.sql +21 -0
- package/app/db/migrations/005_printers.sql +43 -0
- package/app/db/migrations/006_sync.sql +136 -0
- package/app/db/migrations/007_printer_mac.sql +4 -0
- package/app/db/migrations/008_kds.sql +94 -0
- package/app/db/migrations/009_product_categories.sql +20 -0
- package/app/db/migrations/010_printers_sync.sql +43 -0
- package/app/db/migrations/011_team_logos.sql +13 -0
- package/app/db/migrations/012_category_workshop.sql +4 -0
- package/app/monitor.js +185 -0
- package/app/printers/address.js +348 -0
- package/app/proxy.js +20 -3
- package/app/relay/masters.js +173 -0
- package/app/security.js +218 -0
- package/app/socket/auth.js +160 -0
- package/app/socket/request.js +87 -0
- package/app/socket/server.js +121 -19
- package/app/sync/bridge.js +162 -0
- package/app/sync/cloud.js +120 -0
- package/app/sync/config.js +185 -0
- package/app/sync/discovery.js +546 -0
- package/app/sync/index.js +1644 -0
- package/app/sync/kv.js +26 -0
- package/app/sync/lan.js +139 -0
- package/app/sync/lanDevices.js +177 -0
- package/app/sync/lanFront.js +431 -0
- package/app/sync/mirror.js +236 -0
- package/app/sync/push.js +324 -0
- package/app/sync/reconcile.js +161 -0
- package/app/sync/signal.js +202 -0
- package/app/sync/snapshot.js +189 -0
- package/app/sync/store.js +234 -0
- package/app/tcp.js +11 -2
- package/package.json +6 -3
- package/routes/common/api/auth.js +38 -0
- package/routes/common/api/kds.js +37 -0
- package/routes/common/api/orderProducts.js +81 -0
- package/routes/common/api/orders.js +176 -0
- package/routes/common/api/printersCore.js +35 -0
- package/routes/common/api/reports.js +29 -0
- package/routes/common/socket.js +15 -35
- package/routes/common/tax.js +35 -7
- package/routes/desktop/api/offline.js +146 -0
- package/routes/desktop/api/printers.js +288 -5
- package/routes/desktop/local.js +130 -0
- package/routes/server/api/login.js +37 -0
- package/routes/server/api/platform.js +31 -0
- package/routes/server/api/shifts.js +25 -0
- package/routes/server/master.js +24 -0
- package/server.js +111 -4
- package/utils.js +6 -0
- package/tests/usbPrinterTest.js +0 -20
package/app/db/driver.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Драйверы БД ядра: один и тот же SQL исполняется в двух контурах.
|
|
3
|
+
*
|
|
4
|
+
* pgsql — облако («боевой»): та же база, что у Laravel. Node постепенно забирает события,
|
|
5
|
+
* Laravel остаётся рядом как откат, схема — общая (её ведут миграции Laravel).
|
|
6
|
+
* sqlite — локальный мастер: одна касса держит БД, остальные терминалы ходят к ней.
|
|
7
|
+
*
|
|
8
|
+
* Интерфейс намеренно асинхронный ДЛЯ ОБОИХ: pg синхронным быть не может, а держать две
|
|
9
|
+
* версии ядра (sync + async) — гарантированное расхождение в деньгах. SQLite-драйвер внутри
|
|
10
|
+
* остаётся синхронным, наружу отдаёт Promise.
|
|
11
|
+
*
|
|
12
|
+
* Наружу: {dialect, query(sql, params), run(sql, params), exec(sql), transaction(cb), close()}
|
|
13
|
+
* query → массив строк, run → {changes}
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
let driverName = null;
|
|
17
|
+
|
|
18
|
+
// node:sqlite принимает только null | number | bigint | string | Uint8Array.
|
|
19
|
+
// Всё остальное (boolean, Date, undefined, объекты) приводим сами — иначе получим TypeError
|
|
20
|
+
// на ровном месте, причём уже внутри бизнес-логики, а не здесь.
|
|
21
|
+
const normalizeParam = (value) => {
|
|
22
|
+
if (value === undefined || value === null) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
if (typeof value === 'boolean') {
|
|
26
|
+
return value ? 1 : 0;
|
|
27
|
+
}
|
|
28
|
+
if (value instanceof Date) {
|
|
29
|
+
return value.toISOString();
|
|
30
|
+
}
|
|
31
|
+
if (typeof value === 'number' || typeof value === 'bigint' || typeof value === 'string') {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
if (value instanceof Uint8Array || Buffer.isBuffer(value)) {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
return JSON.stringify(value);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const normalizeParams = (params) => params.map(normalizeParam);
|
|
41
|
+
|
|
42
|
+
/** `?` → `$1, $2, ...`: ядро пишет SQL в стиле SQLite, pg требует нумерованные плейсхолдеры. */
|
|
43
|
+
const toPgPlaceholders = (sql) => {
|
|
44
|
+
let index = 0;
|
|
45
|
+
return sql.replace(/\?/g, () => `$${++index}`);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------- SQLite
|
|
49
|
+
|
|
50
|
+
const openSqliteConnection = (file) => {
|
|
51
|
+
try {
|
|
52
|
+
const {DatabaseSync} = require('node:sqlite');
|
|
53
|
+
driverName = 'node:sqlite';
|
|
54
|
+
return new DatabaseSync(file);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
try {
|
|
57
|
+
const BetterSqlite3 = require('better-sqlite3');
|
|
58
|
+
driverName = 'better-sqlite3';
|
|
59
|
+
return new BetterSqlite3(file);
|
|
60
|
+
} catch (e2) {
|
|
61
|
+
const error = new Error(
|
|
62
|
+
'Нет доступного драйвера SQLite: node:sqlite недоступен (' + e.message + '), ' +
|
|
63
|
+
'better-sqlite3 не установлен (' + e2.message + ')'
|
|
64
|
+
);
|
|
65
|
+
error.code = 'NO_SQLITE_DRIVER';
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const openSqlite = (file) => {
|
|
72
|
+
const db = openSqliteConnection(file);
|
|
73
|
+
// Кэш подготовленных выражений: горячий путь (addProduct/calculate) гоняет одни и те же
|
|
74
|
+
// запросы сотни раз за смену, перекомпиляция SQL там лишняя.
|
|
75
|
+
const statements = new Map();
|
|
76
|
+
const prepare = (sql) => {
|
|
77
|
+
if (!statements.has(sql)) {
|
|
78
|
+
statements.set(sql, db.prepare(sql));
|
|
79
|
+
}
|
|
80
|
+
return statements.get(sql);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const driver = {
|
|
84
|
+
dialect: 'sqlite',
|
|
85
|
+
raw: db,
|
|
86
|
+
// Соединение одно на процесс: синхронный блок синка (app/sync/mirror.js) не должен
|
|
87
|
+
// стартовать посреди чужой транзакции, иначе его запись уедет в её COMMIT/ROLLBACK.
|
|
88
|
+
// Свойство DatabaseSync.isTransaction есть не во всех сборках Node — считаем сами.
|
|
89
|
+
inTransaction: false,
|
|
90
|
+
async query(sql, params = []) {
|
|
91
|
+
return prepare(sql).all(...normalizeParams(params));
|
|
92
|
+
},
|
|
93
|
+
async run(sql, params = []) {
|
|
94
|
+
const result = prepare(sql).run(...normalizeParams(params));
|
|
95
|
+
return {changes: result?.changes ?? 0};
|
|
96
|
+
},
|
|
97
|
+
async exec(sql) {
|
|
98
|
+
db.exec(sql);
|
|
99
|
+
},
|
|
100
|
+
/**
|
|
101
|
+
* SQLite однопоточен, вложенных транзакций нет — сам объект драйвера и есть «клиент».
|
|
102
|
+
*/
|
|
103
|
+
async transaction(callback) {
|
|
104
|
+
db.exec('BEGIN');
|
|
105
|
+
driver.inTransaction = true;
|
|
106
|
+
try {
|
|
107
|
+
const result = await callback(driver);
|
|
108
|
+
db.exec('COMMIT');
|
|
109
|
+
return result;
|
|
110
|
+
} catch (e) {
|
|
111
|
+
try {
|
|
112
|
+
db.exec('ROLLBACK');
|
|
113
|
+
} catch (rollbackError) {
|
|
114
|
+
// ROLLBACK может упасть, если транзакция уже свёрнута самим SQLite —
|
|
115
|
+
// подавляем, чтобы не потерять исходную ошибку.
|
|
116
|
+
}
|
|
117
|
+
throw e;
|
|
118
|
+
} finally {
|
|
119
|
+
driver.inTransaction = false;
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
async close() {
|
|
123
|
+
statements.clear();
|
|
124
|
+
db.close();
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return driver;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------- Postgres
|
|
132
|
+
|
|
133
|
+
let pgTypesConfigured = false;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Типы pg приводим к тому же виду, что отдаёт SQLite, — иначе ядро пришлось бы учить
|
|
137
|
+
* различать контуры на каждом чтении.
|
|
138
|
+
* timestamp → строка 'YYYY-MM-DD HH:mm:ss' (Laravel хранит UTC, ядро тоже);
|
|
139
|
+
* numeric/int8 → число (по умолчанию pg отдаёт строку, и суммы бы склеивались строками).
|
|
140
|
+
*/
|
|
141
|
+
const configurePgTypes = (pg) => {
|
|
142
|
+
if (pgTypesConfigured) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const asDateTime = (value) => (value === null ? null : String(value).slice(0, 19));
|
|
146
|
+
pg.types.setTypeParser(1114, asDateTime); // timestamp without time zone
|
|
147
|
+
pg.types.setTypeParser(1184, asDateTime); // timestamp with time zone
|
|
148
|
+
pg.types.setTypeParser(1082, (value) => value); // date
|
|
149
|
+
pg.types.setTypeParser(1700, (value) => (value === null ? null : parseFloat(value))); // numeric
|
|
150
|
+
pg.types.setTypeParser(20, (value) => (value === null ? null : parseInt(value, 10))); // int8
|
|
151
|
+
pgTypesConfigured = true;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const pgConfig = () => {
|
|
155
|
+
if (process.env.CORE_DB_URL) {
|
|
156
|
+
return {connectionString: process.env.CORE_DB_URL};
|
|
157
|
+
}
|
|
158
|
+
// Имена переменных совпадают с .env Laravel намеренно: ops копирует значения как есть.
|
|
159
|
+
return {
|
|
160
|
+
host: process.env.DB_HOST || '127.0.0.1',
|
|
161
|
+
port: Number(process.env.DB_PORT || 5432),
|
|
162
|
+
database: process.env.DB_DATABASE,
|
|
163
|
+
user: process.env.DB_USERNAME,
|
|
164
|
+
password: process.env.DB_PASSWORD,
|
|
165
|
+
max: Number(process.env.CORE_DB_POOL || 10),
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const openPostgres = () => {
|
|
170
|
+
let pg;
|
|
171
|
+
try {
|
|
172
|
+
pg = require('pg');
|
|
173
|
+
} catch (e) {
|
|
174
|
+
const error = new Error('Драйвер pg не установлен: npm i pg (нужен только серверному контуру)');
|
|
175
|
+
error.code = 'NO_PG_DRIVER';
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
configurePgTypes(pg);
|
|
179
|
+
driverName = 'pg';
|
|
180
|
+
|
|
181
|
+
const pool = new pg.Pool(pgConfig());
|
|
182
|
+
|
|
183
|
+
/** Обёртка над клиентом/пулом — одинаковая, чтобы транзакция давала тот же интерфейс. */
|
|
184
|
+
const wrap = (executor, isTransaction = false) => ({
|
|
185
|
+
dialect: 'pgsql',
|
|
186
|
+
raw: executor,
|
|
187
|
+
async query(sql, params = []) {
|
|
188
|
+
const result = await executor.query(toPgPlaceholders(sql), params.map(
|
|
189
|
+
(value) => (value === undefined ? null : value)
|
|
190
|
+
));
|
|
191
|
+
return result.rows;
|
|
192
|
+
},
|
|
193
|
+
async run(sql, params = []) {
|
|
194
|
+
const result = await executor.query(toPgPlaceholders(sql), params.map(
|
|
195
|
+
(value) => (value === undefined ? null : value)
|
|
196
|
+
));
|
|
197
|
+
return {changes: result.rowCount ?? 0};
|
|
198
|
+
},
|
|
199
|
+
async exec(sql) {
|
|
200
|
+
await executor.query(sql);
|
|
201
|
+
},
|
|
202
|
+
async transaction(callback) {
|
|
203
|
+
if (isTransaction) {
|
|
204
|
+
// Вложенную транзакцию не открываем: внешняя уже гарантирует атомарность.
|
|
205
|
+
return callback(wrap(executor, true));
|
|
206
|
+
}
|
|
207
|
+
const client = await pool.connect();
|
|
208
|
+
try {
|
|
209
|
+
await client.query('BEGIN');
|
|
210
|
+
const result = await callback(wrap(client, true));
|
|
211
|
+
await client.query('COMMIT');
|
|
212
|
+
return result;
|
|
213
|
+
} catch (e) {
|
|
214
|
+
try {
|
|
215
|
+
await client.query('ROLLBACK');
|
|
216
|
+
} catch (rollbackError) {
|
|
217
|
+
// соединение могло умереть — исходную ошибку важнее сохранить
|
|
218
|
+
}
|
|
219
|
+
throw e;
|
|
220
|
+
} finally {
|
|
221
|
+
client.release();
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
async close() {
|
|
225
|
+
if (!isTransaction) {
|
|
226
|
+
await pool.end();
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return wrap(pool);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @param {{dialect?: string, file?: string}} options
|
|
236
|
+
* @returns {{dialect: string, query: Function, run: Function, exec: Function, transaction: Function, close: Function}}
|
|
237
|
+
*/
|
|
238
|
+
const openDriver = (options = {}) => {
|
|
239
|
+
const dialect = options.dialect || 'sqlite';
|
|
240
|
+
if (dialect === 'pgsql' || dialect === 'postgres') {
|
|
241
|
+
return openPostgres();
|
|
242
|
+
}
|
|
243
|
+
return openSqlite(options.file);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const getDriverName = () => driverName;
|
|
247
|
+
|
|
248
|
+
module.exports = {openDriver, getDriverName, normalizeParam, toPgPlaceholders};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
-- Снапшот справочников из облака (Laravel остаётся системой записи бэк-офиса).
|
|
2
|
+
-- Локально эти таблицы только читаются доменной логикой заказов; наполняет их синк вниз.
|
|
3
|
+
-- Имена колонок совпадают с Postgres-схемой намеренно: синк должен быть тупым копированием,
|
|
4
|
+
-- а не маппингом полей, иначе расхождение схем всплывёт в деньгах.
|
|
5
|
+
|
|
6
|
+
CREATE TABLE IF NOT EXISTS teams (
|
|
7
|
+
id TEXT PRIMARY KEY,
|
|
8
|
+
name TEXT,
|
|
9
|
+
timezone TEXT,
|
|
10
|
+
-- час окончания бизнес-дня: граница суток для сброса номеров чеков и отчётов
|
|
11
|
+
shift_ends_at INTEGER DEFAULT 0,
|
|
12
|
+
-- код языка команды: Laravel ставит его как локаль приложения (PlatformApiMiddleware),
|
|
13
|
+
-- от него зависит, какой перевод отдаётся в name/full_name товаров
|
|
14
|
+
language_code TEXT DEFAULT 'az',
|
|
15
|
+
settings TEXT,
|
|
16
|
+
synced_at TEXT
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
CREATE TABLE IF NOT EXISTS companies (
|
|
20
|
+
id TEXT PRIMARY KEY,
|
|
21
|
+
team_id TEXT,
|
|
22
|
+
name TEXT,
|
|
23
|
+
service_charge REAL DEFAULT 0,
|
|
24
|
+
synced_at TEXT
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
CREATE TABLE IF NOT EXISTS terminals (
|
|
28
|
+
id TEXT PRIMARY KEY,
|
|
29
|
+
team_id TEXT,
|
|
30
|
+
company_id TEXT,
|
|
31
|
+
name TEXT,
|
|
32
|
+
type TEXT DEFAULT 'standard',
|
|
33
|
+
is_active INTEGER DEFAULT 1,
|
|
34
|
+
is_primary INTEGER DEFAULT 0,
|
|
35
|
+
print_via_terminal_id TEXT,
|
|
36
|
+
synced_at TEXT
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
40
|
+
id TEXT PRIMARY KEY,
|
|
41
|
+
name TEXT,
|
|
42
|
+
synced_at TEXT
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
CREATE TABLE IF NOT EXISTS roles (
|
|
46
|
+
id TEXT PRIMARY KEY,
|
|
47
|
+
team_id TEXT,
|
|
48
|
+
name TEXT,
|
|
49
|
+
is_courier INTEGER DEFAULT 0,
|
|
50
|
+
is_owner INTEGER DEFAULT 0,
|
|
51
|
+
synced_at TEXT
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
CREATE TABLE IF NOT EXISTS team_users (
|
|
55
|
+
id TEXT PRIMARY KEY,
|
|
56
|
+
team_id TEXT,
|
|
57
|
+
user_id TEXT,
|
|
58
|
+
role_id TEXT,
|
|
59
|
+
synced_at TEXT
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
CREATE TABLE IF NOT EXISTS order_sources (
|
|
63
|
+
id TEXT PRIMARY KEY,
|
|
64
|
+
team_id TEXT,
|
|
65
|
+
name TEXT,
|
|
66
|
+
type TEXT DEFAULT 'dine_in',
|
|
67
|
+
show_in_terminal INTEGER DEFAULT 0,
|
|
68
|
+
is_system INTEGER DEFAULT 0,
|
|
69
|
+
synced_at TEXT
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
CREATE TABLE IF NOT EXISTS halls (
|
|
73
|
+
id TEXT PRIMARY KEY,
|
|
74
|
+
team_id TEXT,
|
|
75
|
+
company_id TEXT,
|
|
76
|
+
name TEXT,
|
|
77
|
+
short_name TEXT,
|
|
78
|
+
type TEXT,
|
|
79
|
+
"order" INTEGER DEFAULT 0,
|
|
80
|
+
synced_at TEXT
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
CREATE TABLE IF NOT EXISTS tables (
|
|
84
|
+
id TEXT PRIMARY KEY,
|
|
85
|
+
team_id TEXT,
|
|
86
|
+
company_id TEXT,
|
|
87
|
+
hall_id TEXT,
|
|
88
|
+
label TEXT,
|
|
89
|
+
type TEXT,
|
|
90
|
+
has_deposit INTEGER DEFAULT 0,
|
|
91
|
+
deposit_amount REAL DEFAULT 0,
|
|
92
|
+
"order" INTEGER DEFAULT 0,
|
|
93
|
+
synced_at TEXT
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
CREATE TABLE IF NOT EXISTS units (
|
|
97
|
+
id TEXT PRIMARY KEY,
|
|
98
|
+
name TEXT,
|
|
99
|
+
short_name TEXT,
|
|
100
|
+
code TEXT,
|
|
101
|
+
unit INTEGER DEFAULT 1000,
|
|
102
|
+
synced_at TEXT
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
CREATE TABLE IF NOT EXISTS product_workshops (
|
|
106
|
+
id TEXT PRIMARY KEY,
|
|
107
|
+
team_id TEXT,
|
|
108
|
+
name TEXT,
|
|
109
|
+
print_ticket INTEGER DEFAULT 0,
|
|
110
|
+
config TEXT,
|
|
111
|
+
synced_at TEXT
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
CREATE TABLE IF NOT EXISTS products (
|
|
115
|
+
id TEXT PRIMARY KEY,
|
|
116
|
+
team_id TEXT,
|
|
117
|
+
name TEXT,
|
|
118
|
+
full_name TEXT,
|
|
119
|
+
barcode TEXT,
|
|
120
|
+
color TEXT,
|
|
121
|
+
image TEXT,
|
|
122
|
+
description TEXT,
|
|
123
|
+
is_with_modification INTEGER DEFAULT 0,
|
|
124
|
+
has_portion INTEGER DEFAULT 0,
|
|
125
|
+
extra INTEGER DEFAULT 0,
|
|
126
|
+
extra_type TEXT,
|
|
127
|
+
unit_id TEXT,
|
|
128
|
+
product_category_id TEXT,
|
|
129
|
+
product_workshop_id TEXT,
|
|
130
|
+
type TEXT DEFAULT 'product',
|
|
131
|
+
price REAL DEFAULT 0,
|
|
132
|
+
cost REAL DEFAULT 0,
|
|
133
|
+
measure REAL DEFAULT 1,
|
|
134
|
+
is_not_discount INTEGER DEFAULT 0,
|
|
135
|
+
is_giftable INTEGER DEFAULT 0,
|
|
136
|
+
is_weight_item INTEGER DEFAULT 0,
|
|
137
|
+
ignore_service_charge INTEGER DEFAULT 0,
|
|
138
|
+
ignore_deposit INTEGER DEFAULT 0,
|
|
139
|
+
ignore_workshop INTEGER DEFAULT 0,
|
|
140
|
+
standalone INTEGER DEFAULT 0,
|
|
141
|
+
is_hidden INTEGER DEFAULT 0,
|
|
142
|
+
is_available INTEGER DEFAULT 1,
|
|
143
|
+
is_active INTEGER DEFAULT 1,
|
|
144
|
+
disable_stock INTEGER DEFAULT 0,
|
|
145
|
+
cooking_time INTEGER DEFAULT 0,
|
|
146
|
+
"order" INTEGER DEFAULT 0,
|
|
147
|
+
data TEXT,
|
|
148
|
+
parent_id TEXT,
|
|
149
|
+
deleted_at TEXT,
|
|
150
|
+
synced_at TEXT
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
CREATE INDEX IF NOT EXISTS idx_products_team ON products (team_id);
|
|
154
|
+
CREATE INDEX IF NOT EXISTS idx_products_workshop ON products (product_workshop_id);
|
|
155
|
+
|
|
156
|
+
CREATE TABLE IF NOT EXISTS modifier_sets (
|
|
157
|
+
id TEXT PRIMARY KEY,
|
|
158
|
+
team_id TEXT,
|
|
159
|
+
name TEXT,
|
|
160
|
+
type TEXT,
|
|
161
|
+
description TEXT,
|
|
162
|
+
min INTEGER,
|
|
163
|
+
max INTEGER,
|
|
164
|
+
config TEXT,
|
|
165
|
+
synced_at TEXT
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
CREATE TABLE IF NOT EXISTS product_modifier_sets (
|
|
169
|
+
id TEXT PRIMARY KEY,
|
|
170
|
+
product_id TEXT,
|
|
171
|
+
modifier_set_id TEXT,
|
|
172
|
+
title TEXT,
|
|
173
|
+
description TEXT,
|
|
174
|
+
"order" INTEGER DEFAULT 0,
|
|
175
|
+
config TEXT,
|
|
176
|
+
synced_at TEXT
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
CREATE INDEX IF NOT EXISTS idx_pms_product ON product_modifier_sets (product_id);
|
|
180
|
+
CREATE INDEX IF NOT EXISTS idx_pms_set ON product_modifier_sets (modifier_set_id);
|
|
181
|
+
|
|
182
|
+
CREATE TABLE IF NOT EXISTS modifier_set_modifiers (
|
|
183
|
+
id TEXT PRIMARY KEY,
|
|
184
|
+
modifier_set_id TEXT,
|
|
185
|
+
product_id TEXT,
|
|
186
|
+
unit_id TEXT,
|
|
187
|
+
type TEXT DEFAULT 'product',
|
|
188
|
+
name TEXT,
|
|
189
|
+
image TEXT,
|
|
190
|
+
is_without_debit INTEGER DEFAULT 0,
|
|
191
|
+
"default" INTEGER DEFAULT 0,
|
|
192
|
+
min INTEGER DEFAULT 0,
|
|
193
|
+
max INTEGER DEFAULT 1,
|
|
194
|
+
default_count INTEGER DEFAULT 0,
|
|
195
|
+
price REAL DEFAULT 0,
|
|
196
|
+
cost REAL DEFAULT 0,
|
|
197
|
+
gross REAL DEFAULT 0,
|
|
198
|
+
"order" INTEGER DEFAULT 0,
|
|
199
|
+
description TEXT,
|
|
200
|
+
synced_at TEXT
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
CREATE INDEX IF NOT EXISTS idx_msm_set ON modifier_set_modifiers (modifier_set_id);
|
|
204
|
+
|
|
205
|
+
CREATE TABLE IF NOT EXISTS payment_methods (
|
|
206
|
+
id TEXT PRIMARY KEY,
|
|
207
|
+
team_id TEXT,
|
|
208
|
+
name TEXT,
|
|
209
|
+
type TEXT,
|
|
210
|
+
provider TEXT DEFAULT 'local',
|
|
211
|
+
show_in_terminal INTEGER DEFAULT 0,
|
|
212
|
+
has_change INTEGER DEFAULT 0,
|
|
213
|
+
synced_at TEXT
|
|
214
|
+
);
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
-- Горячий контур: всё, что касса пишет сама и должна уметь писать без интернета.
|
|
2
|
+
-- Истина по открытому чеку здесь; наверх уезжает через outbox (UUID + operation_id уже есть).
|
|
3
|
+
|
|
4
|
+
CREATE TABLE IF NOT EXISTS orders (
|
|
5
|
+
id TEXT PRIMARY KEY,
|
|
6
|
+
global_number INTEGER DEFAULT 1,
|
|
7
|
+
number INTEGER DEFAULT 1,
|
|
8
|
+
opened_at TEXT,
|
|
9
|
+
closed_at TEXT,
|
|
10
|
+
progress INTEGER DEFAULT 0,
|
|
11
|
+
guest_count INTEGER DEFAULT 0,
|
|
12
|
+
comment TEXT,
|
|
13
|
+
status TEXT DEFAULT 'created',
|
|
14
|
+
payed_by_third_party_application INTEGER DEFAULT 0,
|
|
15
|
+
profit REAL DEFAULT 0,
|
|
16
|
+
cost REAL DEFAULT 0,
|
|
17
|
+
fiscal_id TEXT,
|
|
18
|
+
fiscal_url TEXT,
|
|
19
|
+
rrn TEXT,
|
|
20
|
+
subtotal REAL DEFAULT 0,
|
|
21
|
+
service_charge REAL DEFAULT 0,
|
|
22
|
+
service_charge_amount REAL DEFAULT 0,
|
|
23
|
+
deposit_type TEXT,
|
|
24
|
+
deposit_amount REAL DEFAULT 0,
|
|
25
|
+
deposit_gap REAL DEFAULT 0,
|
|
26
|
+
discount_amount REAL DEFAULT 0,
|
|
27
|
+
discount REAL DEFAULT 0,
|
|
28
|
+
max_discount_amount REAL DEFAULT 0,
|
|
29
|
+
discount_type TEXT DEFAULT 'amount',
|
|
30
|
+
tax REAL DEFAULT 0,
|
|
31
|
+
tip REAL DEFAULT 0,
|
|
32
|
+
total REAL DEFAULT 0,
|
|
33
|
+
is_closed_without_payment INTEGER DEFAULT 0,
|
|
34
|
+
without_payment_reason TEXT,
|
|
35
|
+
data TEXT,
|
|
36
|
+
customers TEXT DEFAULT '[1]',
|
|
37
|
+
extras TEXT,
|
|
38
|
+
closing_order_started_at TEXT,
|
|
39
|
+
client_id TEXT,
|
|
40
|
+
table_id TEXT,
|
|
41
|
+
hall_id TEXT,
|
|
42
|
+
team_id TEXT,
|
|
43
|
+
company_id TEXT,
|
|
44
|
+
terminal_id TEXT,
|
|
45
|
+
order_source_id TEXT,
|
|
46
|
+
opened_by_id TEXT,
|
|
47
|
+
closed_by_id TEXT,
|
|
48
|
+
serving_id TEXT,
|
|
49
|
+
shift_id TEXT,
|
|
50
|
+
slot_index INTEGER DEFAULT 1,
|
|
51
|
+
printed_at TEXT,
|
|
52
|
+
should_send_to_kitchen INTEGER DEFAULT 0,
|
|
53
|
+
deleted_at TEXT,
|
|
54
|
+
created_at TEXT,
|
|
55
|
+
updated_at TEXT
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_orders_company_status ON orders (company_id, status);
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_orders_opened_at ON orders (opened_at);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_orders_closed_at ON orders (closed_at);
|
|
61
|
+
|
|
62
|
+
CREATE TABLE IF NOT EXISTS order_products (
|
|
63
|
+
id TEXT PRIMARY KEY,
|
|
64
|
+
order_id TEXT NOT NULL,
|
|
65
|
+
product_id TEXT,
|
|
66
|
+
company_id TEXT,
|
|
67
|
+
team_id TEXT,
|
|
68
|
+
unit_id TEXT,
|
|
69
|
+
guest_number INTEGER DEFAULT 1,
|
|
70
|
+
price REAL DEFAULT 0,
|
|
71
|
+
cost REAL DEFAULT 0,
|
|
72
|
+
profit REAL DEFAULT 0,
|
|
73
|
+
cost_total REAL DEFAULT 0,
|
|
74
|
+
price_total REAL DEFAULT 0,
|
|
75
|
+
quantity REAL DEFAULT 1,
|
|
76
|
+
price_after_service_charge REAL DEFAULT 0,
|
|
77
|
+
price_after_discount REAL DEFAULT 0,
|
|
78
|
+
subtotal REAL DEFAULT 0,
|
|
79
|
+
discount REAL DEFAULT 0,
|
|
80
|
+
discount_amount REAL DEFAULT 0,
|
|
81
|
+
total REAL DEFAULT 0,
|
|
82
|
+
service_charge REAL DEFAULT 0,
|
|
83
|
+
service_charge_amount REAL DEFAULT 0,
|
|
84
|
+
max_discount_amount REAL DEFAULT 0,
|
|
85
|
+
service_charge_calculate_mode TEXT DEFAULT 'after_discount',
|
|
86
|
+
discount_type TEXT DEFAULT 'percent',
|
|
87
|
+
status TEXT DEFAULT 'cooking',
|
|
88
|
+
type TEXT DEFAULT 'product',
|
|
89
|
+
added_at TEXT,
|
|
90
|
+
modified_at TEXT,
|
|
91
|
+
is_gift INTEGER DEFAULT 0,
|
|
92
|
+
stopped_at TEXT,
|
|
93
|
+
is_stopped INTEGER DEFAULT 0,
|
|
94
|
+
is_fixed INTEGER DEFAULT 0,
|
|
95
|
+
paused_seconds INTEGER DEFAULT 0,
|
|
96
|
+
timer_limit_minutes INTEGER,
|
|
97
|
+
comment TEXT,
|
|
98
|
+
modifications_sets_hash TEXT,
|
|
99
|
+
print_quantity REAL DEFAULT 0,
|
|
100
|
+
receipt_print_quantity REAL DEFAULT 0,
|
|
101
|
+
workshop_print_quantity REAL DEFAULT 0,
|
|
102
|
+
kds_print_quantity REAL DEFAULT 0,
|
|
103
|
+
total_workshop_print_quantity REAL DEFAULT 0,
|
|
104
|
+
deleted_at TEXT,
|
|
105
|
+
created_at TEXT,
|
|
106
|
+
updated_at TEXT
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
CREATE INDEX IF NOT EXISTS idx_order_products_order ON order_products (order_id);
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_order_products_merge
|
|
111
|
+
ON order_products (order_id, product_id, guest_number, modifications_sets_hash);
|
|
112
|
+
|
|
113
|
+
CREATE TABLE IF NOT EXISTS order_product_modifier_sets (
|
|
114
|
+
id TEXT PRIMARY KEY,
|
|
115
|
+
order_product_id TEXT NOT NULL,
|
|
116
|
+
modifier_set_id TEXT,
|
|
117
|
+
modifier_set_modifier_id TEXT,
|
|
118
|
+
price REAL DEFAULT 0,
|
|
119
|
+
quantity REAL DEFAULT 0,
|
|
120
|
+
total REAL DEFAULT 0,
|
|
121
|
+
name TEXT,
|
|
122
|
+
modifier_set_title TEXT,
|
|
123
|
+
modifier_set_type TEXT,
|
|
124
|
+
deleted_at TEXT,
|
|
125
|
+
created_at TEXT,
|
|
126
|
+
updated_at TEXT
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
CREATE INDEX IF NOT EXISTS idx_opms_order_product ON order_product_modifier_sets (order_product_id);
|
|
130
|
+
|
|
131
|
+
CREATE TABLE IF NOT EXISTS order_payments (
|
|
132
|
+
id TEXT PRIMARY KEY,
|
|
133
|
+
order_id TEXT NOT NULL,
|
|
134
|
+
payment_method_id TEXT,
|
|
135
|
+
name TEXT,
|
|
136
|
+
amount REAL DEFAULT 0,
|
|
137
|
+
real_amount REAL DEFAULT 0,
|
|
138
|
+
change REAL DEFAULT 0,
|
|
139
|
+
deleted_at TEXT,
|
|
140
|
+
created_at TEXT,
|
|
141
|
+
updated_at TEXT
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
CREATE INDEX IF NOT EXISTS idx_order_payments_order ON order_payments (order_id);
|
|
145
|
+
|
|
146
|
+
CREATE TABLE IF NOT EXISTS order_events (
|
|
147
|
+
id TEXT PRIMARY KEY,
|
|
148
|
+
platform_event_id TEXT,
|
|
149
|
+
event TEXT,
|
|
150
|
+
team_id TEXT,
|
|
151
|
+
team_user_id TEXT,
|
|
152
|
+
order_id TEXT,
|
|
153
|
+
terminal_id TEXT,
|
|
154
|
+
accepted_by TEXT,
|
|
155
|
+
product_id TEXT,
|
|
156
|
+
quantity REAL DEFAULT 0,
|
|
157
|
+
ip_address TEXT,
|
|
158
|
+
data TEXT,
|
|
159
|
+
time_at TEXT,
|
|
160
|
+
deleted_at TEXT,
|
|
161
|
+
created_at TEXT,
|
|
162
|
+
updated_at TEXT
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
CREATE INDEX IF NOT EXISTS idx_order_events_order ON order_events (order_id);
|
|
166
|
+
|
|
167
|
+
-- Очередь мутаций наверх. Дедуп на стороне Laravel уже есть (idempotency_keys),
|
|
168
|
+
-- поэтому здесь достаточно хранить operation_id и повторять до подтверждения.
|
|
169
|
+
CREATE TABLE IF NOT EXISTS outbox (
|
|
170
|
+
id TEXT PRIMARY KEY,
|
|
171
|
+
operation_id TEXT,
|
|
172
|
+
event TEXT NOT NULL,
|
|
173
|
+
payload TEXT NOT NULL,
|
|
174
|
+
headers TEXT,
|
|
175
|
+
status TEXT DEFAULT 'pending',
|
|
176
|
+
attempts INTEGER DEFAULT 0,
|
|
177
|
+
last_error TEXT,
|
|
178
|
+
created_at TEXT,
|
|
179
|
+
sent_at TEXT
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
CREATE INDEX IF NOT EXISTS idx_outbox_status ON outbox (status, created_at);
|
|
183
|
+
|
|
184
|
+
-- Резерв номеров чеков на терминал: офлайн нельзя спрашивать номер у облака,
|
|
185
|
+
-- иначе две кассы выдадут один и тот же (OFFLINE-FIRST-PLAN, Э6).
|
|
186
|
+
CREATE TABLE IF NOT EXISTS number_sequences (
|
|
187
|
+
name TEXT PRIMARY KEY,
|
|
188
|
+
value INTEGER DEFAULT 0,
|
|
189
|
+
updated_at TEXT
|
|
190
|
+
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- Exactly-once для мутаций (БАГ A) в локальном контуре.
|
|
2
|
+
-- Схема повторяет таблицу Laravel (2026_06_24_000001_create_idempotency_keys_table):
|
|
3
|
+
-- в облаке ядро пишет в неё же, поэтому расхождение колонок недопустимо.
|
|
4
|
+
|
|
5
|
+
CREATE TABLE IF NOT EXISTS idempotency_keys (
|
|
6
|
+
operation_id TEXT PRIMARY KEY,
|
|
7
|
+
company_id TEXT,
|
|
8
|
+
event TEXT,
|
|
9
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
10
|
+
response TEXT,
|
|
11
|
+
http_status INTEGER,
|
|
12
|
+
created_at TEXT,
|
|
13
|
+
updated_at TEXT,
|
|
14
|
+
expires_at TEXT
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE INDEX IF NOT EXISTS idx_idempotency_company ON idempotency_keys (company_id);
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_idempotency_expires ON idempotency_keys (expires_at);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- Кассовые смены: приходят сверху синком снапшота (открывают их в облаке), локально нужны
|
|
2
|
+
-- только чтобы закрытый чек сразу получил shift_id — как finalizeClose в Laravel.
|
|
3
|
+
|
|
4
|
+
CREATE TABLE IF NOT EXISTS shifts (
|
|
5
|
+
id TEXT PRIMARY KEY,
|
|
6
|
+
team_id TEXT,
|
|
7
|
+
company_id TEXT,
|
|
8
|
+
type TEXT NOT NULL DEFAULT 'work',
|
|
9
|
+
started_at TEXT,
|
|
10
|
+
ended_at TEXT,
|
|
11
|
+
start_amount REAL NOT NULL DEFAULT 0,
|
|
12
|
+
end_amount REAL NOT NULL DEFAULT 0,
|
|
13
|
+
minutes INTEGER,
|
|
14
|
+
opened_by TEXT,
|
|
15
|
+
closed_by TEXT,
|
|
16
|
+
deleted_at TEXT,
|
|
17
|
+
created_at TEXT,
|
|
18
|
+
updated_at TEXT
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
CREATE INDEX IF NOT EXISTS shifts_open_cash ON shifts (company_id, type, ended_at);
|