work-license-server 1.0.7 → 1.0.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.
- package/assets/landing.js +259 -263
- package/package.json +1 -1
- package/src/landing.js +111 -198
- package/src/server.js +86 -1
- package/src/store.js +109 -15
package/src/store.js
CHANGED
|
@@ -939,9 +939,24 @@ export class LicenseStore {
|
|
|
939
939
|
_formatUserResponse(user) {
|
|
940
940
|
if (!user)
|
|
941
941
|
return null;
|
|
942
|
+
const userEmail = String(user.email || '').toLowerCase();
|
|
943
|
+
const userOrders = Object.values(this.state.orders || {})
|
|
944
|
+
.filter(o => o.status !== 'canceled' && (
|
|
945
|
+
(user.userId && o.ownerAccountId === user.userId)
|
|
946
|
+
|| (user.licenseId && o.licenseId === user.licenseId)
|
|
947
|
+
|| (userEmail && String(o.customer?.email || '').toLowerCase() === userEmail)
|
|
948
|
+
))
|
|
949
|
+
.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
|
950
|
+
const userReservations = Object.values(this.state.reservations || {})
|
|
951
|
+
.filter(r => r.status !== 'canceled' && (
|
|
952
|
+
(user.userId && r.ownerAccountId === user.userId)
|
|
953
|
+
|| (userEmail && String(r.email || '').toLowerCase() === userEmail)
|
|
954
|
+
))
|
|
955
|
+
.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
|
942
956
|
return {
|
|
943
957
|
userId: user.userId,
|
|
944
958
|
email: user.email,
|
|
959
|
+
authProvider: user.authProvider || 'email',
|
|
945
960
|
name: user.name,
|
|
946
961
|
position: user.position || '',
|
|
947
962
|
companyName: user.companyName,
|
|
@@ -960,28 +975,37 @@ export class LicenseStore {
|
|
|
960
975
|
createdAt: user.createdAt,
|
|
961
976
|
lastLoginAt: user.lastLoginAt,
|
|
962
977
|
license: this.getLicenseSummary(user.licenseId),
|
|
978
|
+
orders: userOrders,
|
|
979
|
+
reservations: userReservations,
|
|
963
980
|
};
|
|
964
981
|
}
|
|
965
982
|
|
|
966
983
|
/**
|
|
967
|
-
* Зарегистрировать или авторизовать пользователя личного кабинета ODANT-WORK
|
|
968
|
-
*
|
|
969
|
-
*
|
|
984
|
+
* Зарегистрировать или авторизовать пользователя личного кабинета ODANT-WORK
|
|
985
|
+
* (по email, через OAuth или через мессенджеры Telegram / MAX / VK).
|
|
986
|
+
* Акцепт договора-оферты фиксируется по факту регистрации (ofertaAccepted = true по умолчанию).
|
|
970
987
|
*/
|
|
971
988
|
registerOrLoginUser({
|
|
972
989
|
email = '',
|
|
990
|
+
provider = 'email',
|
|
991
|
+
providerAccount = '',
|
|
973
992
|
name = '',
|
|
974
993
|
position = '',
|
|
975
994
|
companyName = '',
|
|
976
995
|
inn = '',
|
|
977
996
|
phone = '',
|
|
978
997
|
licenseId = '',
|
|
979
|
-
ofertaAccepted =
|
|
998
|
+
ofertaAccepted = true,
|
|
980
999
|
hubUrl = 'https://work.odant.org',
|
|
981
1000
|
} = {}) {
|
|
982
|
-
const
|
|
1001
|
+
const cleanProvider = String(provider || 'email').trim().toLowerCase();
|
|
1002
|
+
let cleanEmail = String(email || '').trim().toLowerCase();
|
|
1003
|
+
if (!cleanEmail && providerAccount) {
|
|
1004
|
+
const safeAcc = String(providerAccount).trim().toLowerCase().replace(/[^a-z0-9._+-]/g, '') || 'user';
|
|
1005
|
+
cleanEmail = safeAcc.includes('@') ? safeAcc : `${safeAcc}@${cleanProvider}.id.odant.org`;
|
|
1006
|
+
}
|
|
983
1007
|
if (!cleanEmail || !cleanEmail.includes('@'))
|
|
984
|
-
throw new Error('Укажите корректный
|
|
1008
|
+
throw new Error('Укажите корректный email или войдите через мессенджер / OAuth');
|
|
985
1009
|
|
|
986
1010
|
let user = this.state.users[cleanEmail];
|
|
987
1011
|
const now = Date.now();
|
|
@@ -995,7 +1019,7 @@ export class LicenseStore {
|
|
|
995
1019
|
ownerEmail: cleanEmail,
|
|
996
1020
|
baseActivated: false,
|
|
997
1021
|
customer: {
|
|
998
|
-
name: companyName || name ||
|
|
1022
|
+
name: companyName || name || cleanEmail,
|
|
999
1023
|
inn: inn || '',
|
|
1000
1024
|
email: cleanEmail,
|
|
1001
1025
|
phone: phone || '',
|
|
@@ -1013,11 +1037,12 @@ export class LicenseStore {
|
|
|
1013
1037
|
user = {
|
|
1014
1038
|
userId,
|
|
1015
1039
|
email: cleanEmail,
|
|
1040
|
+
authProvider: cleanProvider,
|
|
1016
1041
|
name: String(name || companyName || cleanEmail.split('@')[0]).trim(),
|
|
1017
1042
|
position: String(position || '').trim(),
|
|
1018
|
-
companyName: String(companyName ||
|
|
1019
|
-
inn: String(inn ||
|
|
1020
|
-
phone: String(phone ||
|
|
1043
|
+
companyName: String(companyName || '').trim(),
|
|
1044
|
+
inn: String(inn || '').trim(),
|
|
1045
|
+
phone: String(phone || '').trim(),
|
|
1021
1046
|
licenseId: lic.licenseId,
|
|
1022
1047
|
sessionKey: 'wks_' + crypto.randomBytes(16).toString('hex'),
|
|
1023
1048
|
hubToken: tokData.token,
|
|
@@ -1036,6 +1061,7 @@ export class LicenseStore {
|
|
|
1036
1061
|
}
|
|
1037
1062
|
else {
|
|
1038
1063
|
user.lastLoginAt = now;
|
|
1064
|
+
if (cleanProvider && cleanProvider !== 'email') user.authProvider = cleanProvider;
|
|
1039
1065
|
if (name) user.name = String(name).trim();
|
|
1040
1066
|
if (position) user.position = String(position).trim();
|
|
1041
1067
|
if (companyName) user.companyName = String(companyName).trim();
|
|
@@ -1058,11 +1084,11 @@ export class LicenseStore {
|
|
|
1058
1084
|
}
|
|
1059
1085
|
}
|
|
1060
1086
|
|
|
1061
|
-
if (ofertaAccepted && !user.ofertaAccepted) {
|
|
1087
|
+
if (ofertaAccepted !== false && !user.ofertaAccepted) {
|
|
1062
1088
|
user.ofertaAccepted = true;
|
|
1063
1089
|
user.ofertaAcceptedAt = now;
|
|
1064
1090
|
user.ofertaVersion = PUBLIC_OFERTA.version;
|
|
1065
|
-
user.ofertaSignerName = String(name || user.name || user.companyName).trim();
|
|
1091
|
+
user.ofertaSignerName = String(name || user.name || user.companyName || user.email).trim();
|
|
1066
1092
|
user.ofertaSignatureId = 'PEP-' + crypto.randomBytes(4).toString('hex').toUpperCase();
|
|
1067
1093
|
}
|
|
1068
1094
|
|
|
@@ -1299,6 +1325,7 @@ export class LicenseStore {
|
|
|
1299
1325
|
*/
|
|
1300
1326
|
reserveFranchiseSlot({
|
|
1301
1327
|
regionCode = 'RU-SVE',
|
|
1328
|
+
ownerAccountId = '',
|
|
1302
1329
|
companyName = '',
|
|
1303
1330
|
inn = '',
|
|
1304
1331
|
email = '',
|
|
@@ -1306,8 +1333,7 @@ export class LicenseStore {
|
|
|
1306
1333
|
contactPerson = '',
|
|
1307
1334
|
returnUrl = '',
|
|
1308
1335
|
} = {}) {
|
|
1309
|
-
|
|
1310
|
-
throw new Error('Укажите наименование компании для резервирования региональной франшизы');
|
|
1336
|
+
const effCompanyName = String(companyName || email || 'Партнёр ODANT-WORK').trim();
|
|
1311
1337
|
|
|
1312
1338
|
const quotaStatus = this.getRegionPartnerQuotaStatus(regionCode);
|
|
1313
1339
|
if (quotaStatus.vacantFranchises <= 0) {
|
|
@@ -1321,14 +1347,17 @@ export class LicenseStore {
|
|
|
1321
1347
|
const reservationId = 'RES-' + quotaStatus.regionCode.replace(/^RU-/, '') + '-' + crypto.randomBytes(3).toString('hex').toUpperCase();
|
|
1322
1348
|
const depositRub = FRANCHISE_PROGRAM.reservationDepositRub;
|
|
1323
1349
|
const orderId = 'ORD-FR-' + Date.now().toString(36).toUpperCase() + '-' + crypto.randomBytes(2).toString('hex').toUpperCase();
|
|
1350
|
+
const invoiceNumber = 'СЧ-FR-' + new Date(now).toISOString().slice(0, 10).replace(/-/g, '') + '-' + orderId.slice(-4);
|
|
1351
|
+
const contractNumber = 'ДЛ-FR-' + new Date(now).getFullYear() + '-' + orderId.slice(-6);
|
|
1324
1352
|
|
|
1325
1353
|
const reservation = {
|
|
1326
1354
|
reservationId,
|
|
1327
1355
|
orderId,
|
|
1356
|
+
ownerAccountId: String(ownerAccountId || '').trim(),
|
|
1328
1357
|
regionCode: quotaStatus.regionCode,
|
|
1329
1358
|
regionName: quotaStatus.regionName,
|
|
1330
1359
|
district: quotaStatus.district,
|
|
1331
|
-
companyName:
|
|
1360
|
+
companyName: effCompanyName,
|
|
1332
1361
|
inn: String(inn || '').trim(),
|
|
1333
1362
|
email: String(email || 'franchise@odant.org').trim(),
|
|
1334
1363
|
phone: String(phone || '').trim(),
|
|
@@ -1346,6 +1375,7 @@ export class LicenseStore {
|
|
|
1346
1375
|
|
|
1347
1376
|
const order = {
|
|
1348
1377
|
orderId,
|
|
1378
|
+
ownerAccountId: String(ownerAccountId || '').trim(),
|
|
1349
1379
|
kind: 'franchise_reservation',
|
|
1350
1380
|
reservationId,
|
|
1351
1381
|
licenseId: reservationId,
|
|
@@ -1356,6 +1386,9 @@ export class LicenseStore {
|
|
|
1356
1386
|
years: 1,
|
|
1357
1387
|
amountRub: depositRub,
|
|
1358
1388
|
franchiseFeeRub: quotaStatus.franchiseFeeRub,
|
|
1389
|
+
invoiceNumber,
|
|
1390
|
+
contractNumber,
|
|
1391
|
+
invoiceUrl: `/api/orders/${encodeURIComponent(orderId)}/invoice`,
|
|
1359
1392
|
items: [
|
|
1360
1393
|
{
|
|
1361
1394
|
title: `Невозвратный залог за резервирование региональной франшизы ODANT-WORK (${quotaStatus.regionName}, паушальный взнос ${quotaStatus.franchiseFeeRub.toLocaleString('ru-RU')} ₽, полная оплата в течение 1 года)`,
|
|
@@ -1389,6 +1422,67 @@ export class LicenseStore {
|
|
|
1389
1422
|
};
|
|
1390
1423
|
}
|
|
1391
1424
|
|
|
1425
|
+
/**
|
|
1426
|
+
* Обновить способ оплаты и реквизиты юрлица для существующего заказа из Личного кабинета.
|
|
1427
|
+
*/
|
|
1428
|
+
prepareOrderPayment(orderId, {
|
|
1429
|
+
paymentMethod = 'yookassa',
|
|
1430
|
+
companyName = '',
|
|
1431
|
+
inn = '',
|
|
1432
|
+
kpp = '',
|
|
1433
|
+
legalAddress = '',
|
|
1434
|
+
} = {}) {
|
|
1435
|
+
const order = this.state.orders[orderId];
|
|
1436
|
+
if (!order)
|
|
1437
|
+
throw new Error(`Заказ ${orderId} не найден`);
|
|
1438
|
+
order.paymentMethod = paymentMethod === 'invoice' ? 'invoice' : 'yookassa';
|
|
1439
|
+
order.customer = order.customer || {};
|
|
1440
|
+
if (companyName) order.customer.name = String(companyName).trim();
|
|
1441
|
+
if (inn) order.customer.inn = String(inn).trim();
|
|
1442
|
+
if (kpp) order.customer.kpp = String(kpp).trim();
|
|
1443
|
+
if (legalAddress) order.customer.legalAddress = String(legalAddress).trim();
|
|
1444
|
+
if (!order.invoiceNumber) {
|
|
1445
|
+
order.invoiceNumber = 'СЧ-' + new Date().toISOString().slice(0, 10).replace(/-/g, '') + '-' + orderId.slice(-4);
|
|
1446
|
+
}
|
|
1447
|
+
if (!order.contractNumber) {
|
|
1448
|
+
order.contractNumber = 'ДЛ-' + new Date().getFullYear() + '-' + orderId.slice(-6);
|
|
1449
|
+
}
|
|
1450
|
+
if (!order.invoiceUrl) {
|
|
1451
|
+
order.invoiceUrl = `/api/orders/${encodeURIComponent(orderId)}/invoice`;
|
|
1452
|
+
}
|
|
1453
|
+
if (order.reservationId && this.state.reservations[order.reservationId]) {
|
|
1454
|
+
if (companyName) this.state.reservations[order.reservationId].companyName = String(companyName).trim();
|
|
1455
|
+
if (inn) this.state.reservations[order.reservationId].inn = String(inn).trim();
|
|
1456
|
+
}
|
|
1457
|
+
if (order.ownerAccountId) {
|
|
1458
|
+
const u = Object.values(this.state.users).find(usr => usr.userId === order.ownerAccountId);
|
|
1459
|
+
if (u) {
|
|
1460
|
+
if (companyName) u.companyName = String(companyName).trim();
|
|
1461
|
+
if (inn) u.inn = String(inn).trim();
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
this._save();
|
|
1465
|
+
return order;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
/**
|
|
1469
|
+
* Отменить неоплаченный заказ в Личном кабинете (и снять резерв региона, если это был резерв франшизы).
|
|
1470
|
+
*/
|
|
1471
|
+
cancelOrder(orderId) {
|
|
1472
|
+
const order = this.state.orders[orderId];
|
|
1473
|
+
if (!order)
|
|
1474
|
+
throw new Error(`Заказ ${orderId} не найден`);
|
|
1475
|
+
if (order.status === 'paid')
|
|
1476
|
+
throw new Error('Оплаченный заказ отменить нельзя');
|
|
1477
|
+
order.status = 'canceled';
|
|
1478
|
+
order.canceledAt = Date.now();
|
|
1479
|
+
if (order.reservationId && this.state.reservations[order.reservationId]) {
|
|
1480
|
+
this.state.reservations[order.reservationId].status = 'canceled';
|
|
1481
|
+
}
|
|
1482
|
+
this._save();
|
|
1483
|
+
return { ok: true, orderId };
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1392
1486
|
/**
|
|
1393
1487
|
* Зарегистрировать регионального партнёра-франчайзи с контролем региональной квоты (по населению)
|
|
1394
1488
|
* и расчётом фиксированного паушального взноса (пропорционально экономике региона).
|