work-license-server 1.0.0 → 1.0.2
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/README.md +29 -14
- package/assets/landing.css +609 -0
- package/assets/landing.js +1187 -0
- package/assets/odant-logo.png +0 -0
- package/assets/russia-regions.json +1 -0
- package/package.json +2 -1
- package/src/agent.js +195 -0
- package/src/bots.js +2 -2
- package/src/catalog.js +319 -93
- package/src/index.js +6 -0
- package/src/landing.js +746 -882
- package/src/server.js +103 -5
- package/src/store.js +202 -2
package/src/server.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTTP-сервер приложения work.odant.org:
|
|
3
|
-
* - GET / —
|
|
4
|
-
* - GET /
|
|
3
|
+
* - GET / — Витрина ODANT-WORK (платформа, ИИ-агенты, тарифы, заказ, франшиза, поддержка);
|
|
4
|
+
* - GET /favicon.ico, /assets/* — Логотип ODANT, стили и скрипт витрины, контуры субъектов РФ;
|
|
5
|
+
* - GET /api/catalog — Спецификация комплекса (AMD Ryzen™ AI Max+ PRO 495, 192 ГБ, 4 ТБ) и тарифы Start/Business/Enterprise;
|
|
5
6
|
* - POST /api/quote — Расчёт стоимости заказа;
|
|
6
7
|
* - POST /api/license/issue — Выпуск базовой лицензии ПАК (5 вечных мест + 1 год поддержки);
|
|
7
8
|
* - POST /api/license/sync — Онлайн-синхронизация лицензии с ПАК клиента (привязка HWID + node-key.json);
|
|
@@ -16,9 +17,13 @@
|
|
|
16
17
|
* - GET /api/admin/overview — Сводка по лицензиям, заказам и тикетам.
|
|
17
18
|
*/
|
|
18
19
|
import http from 'node:http';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import crypto from 'node:crypto';
|
|
19
22
|
import {
|
|
20
23
|
PAK_PRODUCT,
|
|
21
24
|
SLA_TIERS,
|
|
25
|
+
SEAT_VOLUME_DISCOUNTS,
|
|
26
|
+
calculateSeatVolumeDiscount,
|
|
22
27
|
calculateOrderQuote,
|
|
23
28
|
FRANCHISE_PROGRAM,
|
|
24
29
|
REGIONS_CATALOG,
|
|
@@ -31,6 +36,7 @@ import {
|
|
|
31
36
|
import { LicenseStore } from './store.js';
|
|
32
37
|
import { YooKassaService } from './yookassa.js';
|
|
33
38
|
import { SupportBotsHub } from './bots.js';
|
|
39
|
+
import { LicenseServerAiAgent } from './agent.js';
|
|
34
40
|
import { renderLandingPage, renderSandboxPaymentPage } from './landing.js';
|
|
35
41
|
|
|
36
42
|
async function readJsonBody(req, maxBytes = 1024 * 1024) {
|
|
@@ -59,6 +65,42 @@ function sendJson(res, status, data) {
|
|
|
59
65
|
res.end(body);
|
|
60
66
|
}
|
|
61
67
|
|
|
68
|
+
const ASSETS_DIR = new URL('../assets/', import.meta.url);
|
|
69
|
+
const STATIC_ASSETS = Object.freeze({
|
|
70
|
+
'/favicon.ico': { file: 'odant-logo.png', type: 'image/png' },
|
|
71
|
+
'/assets/odant-logo.png': { file: 'odant-logo.png', type: 'image/png' },
|
|
72
|
+
'/assets/russia-regions.json': { file: 'russia-regions.json', type: 'application/json; charset=utf-8' },
|
|
73
|
+
'/assets/landing.css': { file: 'landing.css', type: 'text/css; charset=utf-8' },
|
|
74
|
+
'/assets/landing.js': { file: 'landing.js', type: 'text/javascript; charset=utf-8' },
|
|
75
|
+
});
|
|
76
|
+
const assetCache = new Map();
|
|
77
|
+
let assetVersionCache = '';
|
|
78
|
+
|
|
79
|
+
/** Версия статики для сброса кэша браузера: короткий хеш стилей и скрипта витрины. */
|
|
80
|
+
function assetVersion() {
|
|
81
|
+
if (assetVersionCache && !process.env.WORK_DEV)
|
|
82
|
+
return assetVersionCache;
|
|
83
|
+
const hash = crypto.createHash('sha1');
|
|
84
|
+
for (const file of ['landing.css', 'landing.js'])
|
|
85
|
+
hash.update(fs.readFileSync(new URL(file, ASSETS_DIR)));
|
|
86
|
+
assetVersionCache = hash.digest('hex').slice(0, 10);
|
|
87
|
+
return assetVersionCache;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function sendStaticAsset(res, asset) {
|
|
91
|
+
let body = assetCache.get(asset.file);
|
|
92
|
+
if (!body) {
|
|
93
|
+
body = fs.readFileSync(new URL(asset.file, ASSETS_DIR));
|
|
94
|
+
if (!process.env.WORK_DEV)
|
|
95
|
+
assetCache.set(asset.file, body);
|
|
96
|
+
}
|
|
97
|
+
res.writeHead(200, {
|
|
98
|
+
'Content-Type': asset.type,
|
|
99
|
+
'Cache-Control': process.env.WORK_DEV ? 'no-cache' : 'public, max-age=3600',
|
|
100
|
+
});
|
|
101
|
+
res.end(body);
|
|
102
|
+
}
|
|
103
|
+
|
|
62
104
|
function sendHtml(res, status, html) {
|
|
63
105
|
res.writeHead(status, {
|
|
64
106
|
'Content-Type': 'text/html; charset=utf-8',
|
|
@@ -89,6 +131,11 @@ export function createLicenseServerApp(options = {}) {
|
|
|
89
131
|
publicOrigin: options.publicOrigin || 'https://work.odant.org',
|
|
90
132
|
fetchImpl: options.fetchImpl,
|
|
91
133
|
});
|
|
134
|
+
const agent = options.agent || new LicenseServerAiAgent({
|
|
135
|
+
store,
|
|
136
|
+
yookassa,
|
|
137
|
+
fetchImpl: options.fetchImpl,
|
|
138
|
+
});
|
|
92
139
|
|
|
93
140
|
async function handler(req, res) {
|
|
94
141
|
try {
|
|
@@ -108,9 +155,19 @@ export function createLicenseServerApp(options = {}) {
|
|
|
108
155
|
const url = new URL(req.url || '/', reqOrigin);
|
|
109
156
|
const pathname = url.pathname;
|
|
110
157
|
|
|
111
|
-
// 1. Главная страница лендинга work.odant.org
|
|
112
|
-
if (req.method === 'GET' && (pathname === '/' || pathname === '/index.html')) {
|
|
113
|
-
return sendHtml(res, 200, renderLandingPage({
|
|
158
|
+
// 1. Главная страница лендинга work.odant.org и панель администратора (/admin)
|
|
159
|
+
if (req.method === 'GET' && (pathname === '/' || pathname === '/index.html' || pathname === '/admin')) {
|
|
160
|
+
return sendHtml(res, 200, renderLandingPage({
|
|
161
|
+
publicKey: store.authorityKeys.publicKey,
|
|
162
|
+
regions: store.listRegionsWithQuotas(),
|
|
163
|
+
assetVersion: assetVersion(),
|
|
164
|
+
initialStage: pathname === '/admin' ? 'admin' : '',
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 1.1 Статика лендинга: логотип ODANT, стили, клиентский скрипт и контуры субъектов РФ
|
|
169
|
+
if (req.method === 'GET' && STATIC_ASSETS[pathname]) {
|
|
170
|
+
return sendStaticAsset(res, STATIC_ASSETS[pathname]);
|
|
114
171
|
}
|
|
115
172
|
|
|
116
173
|
// 2. Каталог ПАК, тарифов SLA и партнёрской франшизы
|
|
@@ -118,6 +175,7 @@ export function createLicenseServerApp(options = {}) {
|
|
|
118
175
|
return sendJson(res, 200, {
|
|
119
176
|
product: PAK_PRODUCT,
|
|
120
177
|
tiers: Object.values(SLA_TIERS),
|
|
178
|
+
volumeDiscounts: SEAT_VOLUME_DISCOUNTS,
|
|
121
179
|
franchiseProgram: FRANCHISE_PROGRAM,
|
|
122
180
|
regions: store.listRegionsWithQuotas(),
|
|
123
181
|
trainingTracks: PARTNER_TRAINING_TRACKS,
|
|
@@ -125,6 +183,22 @@ export function createLicenseServerApp(options = {}) {
|
|
|
125
183
|
});
|
|
126
184
|
}
|
|
127
185
|
|
|
186
|
+
// 2.0 Вход / регистрация пользователя (для доступа к разделу докупки пользовательских лицензий)
|
|
187
|
+
if (req.method === 'POST' && (pathname === '/api/auth/login' || pathname === '/api/auth/register')) {
|
|
188
|
+
const body = await readJsonBody(req);
|
|
189
|
+
const user = store.registerOrLoginUser(body);
|
|
190
|
+
return sendJson(res, 200, user);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (req.method === 'GET' && pathname === '/api/auth/me') {
|
|
194
|
+
const authHeader = String(req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
|
|
195
|
+
const key = url.searchParams.get('sessionKey') || authHeader;
|
|
196
|
+
const user = store.findUserBySessionKey(key);
|
|
197
|
+
if (!user)
|
|
198
|
+
return sendJson(res, 401, { authenticated: false });
|
|
199
|
+
return sendJson(res, 200, { authenticated: true, user });
|
|
200
|
+
}
|
|
201
|
+
|
|
128
202
|
// 2.1 Партнёрская программа: справочник регионов с квотами по населению, шкала скидок 10%-50% и курсы сертификации
|
|
129
203
|
if (req.method === 'GET' && pathname === '/api/partners/program') {
|
|
130
204
|
return sendJson(res, 200, {
|
|
@@ -164,6 +238,28 @@ export function createLicenseServerApp(options = {}) {
|
|
|
164
238
|
return sendJson(res, 200, result);
|
|
165
239
|
}
|
|
166
240
|
|
|
241
|
+
// 2.6 Резервирование региональной франшизы с оплатой невозвратного залога 100 000 ₽ через ЮKassa
|
|
242
|
+
if (req.method === 'POST' && pathname === '/api/partners/reserve') {
|
|
243
|
+
const body = await readJsonBody(req);
|
|
244
|
+
const reserved = store.reserveFranchiseSlot(body);
|
|
245
|
+
const payment = await yookassa.createPaymentForOrder(reserved.order, { originOverride: reqOrigin });
|
|
246
|
+
return sendJson(res, 200, {
|
|
247
|
+
...payment,
|
|
248
|
+
reservation: reserved.reservation,
|
|
249
|
+
order: reserved.order,
|
|
250
|
+
regionQuota: reserved.regionQuota,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 2.7 Собственный ИИ-агент лицензионного сервера ODANT-WORK
|
|
255
|
+
if (req.method === 'POST' && pathname === '/api/agent/chat') {
|
|
256
|
+
const body = await readJsonBody(req);
|
|
257
|
+
const authHeader = String(req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
|
|
258
|
+
const user = store.findUserBySessionKey(body.sessionKey || authHeader);
|
|
259
|
+
const reply = await agent.chat({ message: body.message || body.text || '', history: body.history || [], user });
|
|
260
|
+
return sendJson(res, 200, reply);
|
|
261
|
+
}
|
|
262
|
+
|
|
167
263
|
// 3. Калькулятор стоимости
|
|
168
264
|
if (req.method === 'POST' && pathname === '/api/quote') {
|
|
169
265
|
const body = await readJsonBody(req);
|
|
@@ -263,6 +359,7 @@ export function createLicenseServerApp(options = {}) {
|
|
|
263
359
|
tickets: store.listTickets(),
|
|
264
360
|
orders: Object.values(store.state.orders),
|
|
265
361
|
partners: store.listPartners(),
|
|
362
|
+
reservations: Object.values(store.state.reservations || {}),
|
|
266
363
|
regions: store.listRegionsWithQuotas(),
|
|
267
364
|
});
|
|
268
365
|
}
|
|
@@ -278,6 +375,7 @@ export function createLicenseServerApp(options = {}) {
|
|
|
278
375
|
store,
|
|
279
376
|
yookassa,
|
|
280
377
|
bots,
|
|
378
|
+
agent,
|
|
281
379
|
handler,
|
|
282
380
|
listen(port = Number(process.env.PORT || 8020), host = '0.0.0.0') {
|
|
283
381
|
const server = http.createServer(handler);
|
package/src/store.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
PAK_PRODUCT,
|
|
13
13
|
SLA_TIERS,
|
|
14
14
|
calculateOrderQuote,
|
|
15
|
+
FRANCHISE_PROGRAM,
|
|
15
16
|
REGIONS_CATALOG,
|
|
16
17
|
PARTNER_TRAINING_TRACKS,
|
|
17
18
|
calculateFranchiseFee,
|
|
@@ -35,7 +36,9 @@ export class LicenseStore {
|
|
|
35
36
|
tickets: {},
|
|
36
37
|
botBindings: {},
|
|
37
38
|
partners: {},
|
|
39
|
+
reservations: {},
|
|
38
40
|
certificates: {},
|
|
41
|
+
users: {},
|
|
39
42
|
};
|
|
40
43
|
this._load();
|
|
41
44
|
}
|
|
@@ -51,7 +54,9 @@ export class LicenseStore {
|
|
|
51
54
|
this.state.tickets = raw.tickets || {};
|
|
52
55
|
this.state.botBindings = raw.botBindings || {};
|
|
53
56
|
this.state.partners = raw.partners || {};
|
|
57
|
+
this.state.reservations = raw.reservations || {};
|
|
54
58
|
this.state.certificates = raw.certificates || {};
|
|
59
|
+
this.state.users = raw.users || {};
|
|
55
60
|
}
|
|
56
61
|
}
|
|
57
62
|
catch { /* начинаем с чистого состояния */ }
|
|
@@ -427,6 +432,21 @@ export class LicenseStore {
|
|
|
427
432
|
if (paymentId)
|
|
428
433
|
ord.paymentId = paymentId;
|
|
429
434
|
|
|
435
|
+
if (ord.kind === 'franchise_reservation' && ord.reservationId && this.state.reservations[ord.reservationId]) {
|
|
436
|
+
const resv = this.state.reservations[ord.reservationId];
|
|
437
|
+
resv.depositPaid = true;
|
|
438
|
+
resv.paidAt = ord.paidAt;
|
|
439
|
+
resv.paymentId = ord.paymentId;
|
|
440
|
+
resv.status = 'deposit_paid';
|
|
441
|
+
this._save();
|
|
442
|
+
return {
|
|
443
|
+
order: ord,
|
|
444
|
+
reservation: resv,
|
|
445
|
+
regionQuota: this.getRegionPartnerQuotaStatus(resv.regionCode),
|
|
446
|
+
alreadyProcessed: false,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
430
450
|
const license = this.grantAddonSubscription(ord.licenseId, {
|
|
431
451
|
tier: ord.tier,
|
|
432
452
|
seats: ord.addonSeats,
|
|
@@ -569,17 +589,197 @@ export class LicenseStore {
|
|
|
569
589
|
}
|
|
570
590
|
|
|
571
591
|
/**
|
|
572
|
-
*
|
|
592
|
+
* Зарегистрировать или авторизовать пользователя личного кабинета ODANT-WORK.
|
|
593
|
+
* Докупка дополнительных пользовательских лицензий доступна только зарегистрированным пользователям.
|
|
594
|
+
*/
|
|
595
|
+
registerOrLoginUser({ email = '', name = '', companyName = '', inn = '', licenseId = '' } = {}) {
|
|
596
|
+
const cleanEmail = String(email || '').trim().toLowerCase();
|
|
597
|
+
if (!cleanEmail || !cleanEmail.includes('@'))
|
|
598
|
+
throw new Error('Укажите корректный email для входа в личный кабинет ODANT-WORK');
|
|
599
|
+
|
|
600
|
+
let user = this.state.users[cleanEmail];
|
|
601
|
+
const now = Date.now();
|
|
602
|
+
if (!user) {
|
|
603
|
+
let lic = licenseId ? this.findLicense(licenseId) : null;
|
|
604
|
+
if (!lic) {
|
|
605
|
+
const summary = this.issueOrUpdateLicense({
|
|
606
|
+
customer: {
|
|
607
|
+
name: companyName || name || 'Организация ODANT-WORK',
|
|
608
|
+
inn: inn || '',
|
|
609
|
+
email: cleanEmail,
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
lic = summary.certificate;
|
|
613
|
+
}
|
|
614
|
+
user = {
|
|
615
|
+
userId: 'USR-' + crypto.createHash('sha256').update(cleanEmail).digest('hex').slice(0, 10).toUpperCase(),
|
|
616
|
+
email: cleanEmail,
|
|
617
|
+
name: String(name || companyName || cleanEmail.split('@')[0]).trim(),
|
|
618
|
+
companyName: String(companyName || lic.customer?.name || 'Организация ODANT-WORK').trim(),
|
|
619
|
+
inn: String(inn || lic.customer?.inn || '').trim(),
|
|
620
|
+
licenseId: lic.licenseId,
|
|
621
|
+
sessionKey: 'wks_' + crypto.randomBytes(16).toString('hex'),
|
|
622
|
+
createdAt: now,
|
|
623
|
+
lastLoginAt: now,
|
|
624
|
+
};
|
|
625
|
+
this.state.users[cleanEmail] = user;
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
user.lastLoginAt = now;
|
|
629
|
+
if (name) user.name = String(name).trim();
|
|
630
|
+
if (companyName) user.companyName = String(companyName).trim();
|
|
631
|
+
if (inn) user.inn = String(inn).trim();
|
|
632
|
+
if (licenseId && this.findLicense(licenseId)) {
|
|
633
|
+
user.licenseId = this.findLicense(licenseId).licenseId;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
this._save();
|
|
637
|
+
return {
|
|
638
|
+
userId: user.userId,
|
|
639
|
+
email: user.email,
|
|
640
|
+
name: user.name,
|
|
641
|
+
companyName: user.companyName,
|
|
642
|
+
inn: user.inn,
|
|
643
|
+
licenseId: user.licenseId,
|
|
644
|
+
sessionKey: user.sessionKey,
|
|
645
|
+
license: this.getLicenseSummary(user.licenseId),
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
findUserBySessionKey(sessionKey = '') {
|
|
650
|
+
const key = String(sessionKey || '').trim();
|
|
651
|
+
if (!key)
|
|
652
|
+
return null;
|
|
653
|
+
const user = Object.values(this.state.users).find(u => u.sessionKey === key);
|
|
654
|
+
if (!user)
|
|
655
|
+
return null;
|
|
656
|
+
return {
|
|
657
|
+
userId: user.userId,
|
|
658
|
+
email: user.email,
|
|
659
|
+
name: user.name,
|
|
660
|
+
companyName: user.companyName,
|
|
661
|
+
inn: user.inn,
|
|
662
|
+
licenseId: user.licenseId,
|
|
663
|
+
license: this.getLicenseSummary(user.licenseId),
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Получить статус квоты и вакантных франшиз по региону (макс. число партнёров пропорционально населению).
|
|
573
669
|
*/
|
|
574
670
|
getRegionPartnerQuotaStatus(regionCode = 'RU-SVE', customPopulationMillions = null) {
|
|
575
671
|
const info = calculateFranchiseFee(regionCode, customPopulationMillions);
|
|
576
672
|
const existingInRegion = Object.values(this.state.partners).filter(
|
|
577
673
|
p => p.regionCode === info.regionCode && p.status !== 'revoked'
|
|
578
674
|
);
|
|
675
|
+
const reservedInRegion = Object.values(this.state.reservations || {}).filter(
|
|
676
|
+
r => r.regionCode === info.regionCode && r.status !== 'canceled' && !r.convertedToPartnerId
|
|
677
|
+
);
|
|
678
|
+
const occupied = existingInRegion.length + reservedInRegion.length;
|
|
679
|
+
const vacantFranchises = Math.max(0, info.maxPartnersQuota - occupied);
|
|
579
680
|
return {
|
|
580
681
|
...info,
|
|
581
682
|
activePartnersCount: existingInRegion.length,
|
|
582
|
-
|
|
683
|
+
reservedFranchisesCount: reservedInRegion.length,
|
|
684
|
+
occupiedFranchisesCount: occupied,
|
|
685
|
+
availablePartnerSlots: vacantFranchises,
|
|
686
|
+
vacantFranchises,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Зарезервировать региональную франшизу с выставлением счёта ЮKassa на невозвратный залог 100 000 ₽.
|
|
692
|
+
* Полный паушальный взнос (500 000 ₽ × индекс региона) фиксируется в договоре резерва, полная оплата — в течение 1 года.
|
|
693
|
+
*/
|
|
694
|
+
reserveFranchiseSlot({
|
|
695
|
+
regionCode = 'RU-SVE',
|
|
696
|
+
companyName = '',
|
|
697
|
+
inn = '',
|
|
698
|
+
email = '',
|
|
699
|
+
phone = '',
|
|
700
|
+
contactPerson = '',
|
|
701
|
+
returnUrl = '',
|
|
702
|
+
} = {}) {
|
|
703
|
+
if (!companyName)
|
|
704
|
+
throw new Error('Укажите наименование компании для резервирования региональной франшизы');
|
|
705
|
+
|
|
706
|
+
const quotaStatus = this.getRegionPartnerQuotaStatus(regionCode);
|
|
707
|
+
if (quotaStatus.vacantFranchises <= 0) {
|
|
708
|
+
throw new Error(
|
|
709
|
+
`В регионе «${quotaStatus.regionName}» нет вакантных франшиз `
|
|
710
|
+
+ `(занято ${quotaStatus.occupiedFranchisesCount} из ${quotaStatus.maxPartnersQuota}, пропорционально населению ${quotaStatus.populationMillions} млн чел.)`
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const now = Date.now();
|
|
715
|
+
const reservationId = 'RES-' + quotaStatus.regionCode.replace(/^RU-/, '') + '-' + crypto.randomBytes(3).toString('hex').toUpperCase();
|
|
716
|
+
const depositRub = FRANCHISE_PROGRAM.reservationDepositRub;
|
|
717
|
+
const orderId = 'ORD-FR-' + Date.now().toString(36).toUpperCase() + '-' + crypto.randomBytes(2).toString('hex').toUpperCase();
|
|
718
|
+
|
|
719
|
+
const reservation = {
|
|
720
|
+
reservationId,
|
|
721
|
+
orderId,
|
|
722
|
+
regionCode: quotaStatus.regionCode,
|
|
723
|
+
regionName: quotaStatus.regionName,
|
|
724
|
+
district: quotaStatus.district,
|
|
725
|
+
companyName: String(companyName).trim(),
|
|
726
|
+
inn: String(inn || '').trim(),
|
|
727
|
+
email: String(email || 'franchise@odant.org').trim(),
|
|
728
|
+
phone: String(phone || '').trim(),
|
|
729
|
+
contactPerson: String(contactPerson || '').trim(),
|
|
730
|
+
franchiseFeeRub: quotaStatus.franchiseFeeRub,
|
|
731
|
+
nonRefundableDepositRub: depositRub,
|
|
732
|
+
remainingFranchiseFeeRub: Math.max(0, quotaStatus.franchiseFeeRub - depositRub),
|
|
733
|
+
fullPaymentPeriodMonths: FRANCHISE_PROGRAM.fullPaymentPeriodMonths,
|
|
734
|
+
fullPaymentDeadlineAt: now + 365 * 24 * 3600_000,
|
|
735
|
+
status: 'reserved_pending_deposit',
|
|
736
|
+
depositPaid: false,
|
|
737
|
+
createdAt: now,
|
|
738
|
+
expiresAt: now + 365 * 24 * 3600_000,
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
const order = {
|
|
742
|
+
orderId,
|
|
743
|
+
kind: 'franchise_reservation',
|
|
744
|
+
reservationId,
|
|
745
|
+
licenseId: reservationId,
|
|
746
|
+
regionCode: quotaStatus.regionCode,
|
|
747
|
+
regionName: quotaStatus.regionName,
|
|
748
|
+
tierName: `Резерв франшизы (${quotaStatus.regionName})`,
|
|
749
|
+
addonSeats: 1,
|
|
750
|
+
years: 1,
|
|
751
|
+
amountRub: depositRub,
|
|
752
|
+
franchiseFeeRub: quotaStatus.franchiseFeeRub,
|
|
753
|
+
items: [
|
|
754
|
+
{
|
|
755
|
+
title: `Невозвратный залог за резервирование региональной франшизы ODANT-WORK (${quotaStatus.regionName}, паушальный взнос ${quotaStatus.franchiseFeeRub.toLocaleString('ru-RU')} ₽, полная оплата в течение 1 года)`,
|
|
756
|
+
quantity: 1,
|
|
757
|
+
unitPriceRub: depositRub,
|
|
758
|
+
totalRub: depositRub,
|
|
759
|
+
},
|
|
760
|
+
],
|
|
761
|
+
customer: {
|
|
762
|
+
name: reservation.companyName,
|
|
763
|
+
inn: reservation.inn,
|
|
764
|
+
email: reservation.email,
|
|
765
|
+
phone: reservation.phone,
|
|
766
|
+
},
|
|
767
|
+
status: 'pending',
|
|
768
|
+
paymentId: '',
|
|
769
|
+
confirmationUrl: '',
|
|
770
|
+
returnUrl: returnUrl || 'https://work.odant.org',
|
|
771
|
+
createdAt: now,
|
|
772
|
+
paidAt: null,
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
this.state.reservations[reservationId] = reservation;
|
|
776
|
+
this.state.orders[orderId] = order;
|
|
777
|
+
this._save();
|
|
778
|
+
|
|
779
|
+
return {
|
|
780
|
+
reservation,
|
|
781
|
+
order,
|
|
782
|
+
regionQuota: this.getRegionPartnerQuotaStatus(quotaStatus.regionCode),
|
|
583
783
|
};
|
|
584
784
|
}
|
|
585
785
|
|