work-license-server 1.0.6 → 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 +309 -228
- package/package.json +1 -1
- package/src/landing.js +123 -179
- package/src/server.js +86 -1
- package/src/store.js +110 -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();
|
|
@@ -993,8 +1017,9 @@ export class LicenseStore {
|
|
|
993
1017
|
const summary = this.issueOrUpdateLicense({
|
|
994
1018
|
ownerAccountId: userId,
|
|
995
1019
|
ownerEmail: cleanEmail,
|
|
1020
|
+
baseActivated: false,
|
|
996
1021
|
customer: {
|
|
997
|
-
name: companyName || name ||
|
|
1022
|
+
name: companyName || name || cleanEmail,
|
|
998
1023
|
inn: inn || '',
|
|
999
1024
|
email: cleanEmail,
|
|
1000
1025
|
phone: phone || '',
|
|
@@ -1012,11 +1037,12 @@ export class LicenseStore {
|
|
|
1012
1037
|
user = {
|
|
1013
1038
|
userId,
|
|
1014
1039
|
email: cleanEmail,
|
|
1040
|
+
authProvider: cleanProvider,
|
|
1015
1041
|
name: String(name || companyName || cleanEmail.split('@')[0]).trim(),
|
|
1016
1042
|
position: String(position || '').trim(),
|
|
1017
|
-
companyName: String(companyName ||
|
|
1018
|
-
inn: String(inn ||
|
|
1019
|
-
phone: String(phone ||
|
|
1043
|
+
companyName: String(companyName || '').trim(),
|
|
1044
|
+
inn: String(inn || '').trim(),
|
|
1045
|
+
phone: String(phone || '').trim(),
|
|
1020
1046
|
licenseId: lic.licenseId,
|
|
1021
1047
|
sessionKey: 'wks_' + crypto.randomBytes(16).toString('hex'),
|
|
1022
1048
|
hubToken: tokData.token,
|
|
@@ -1035,6 +1061,7 @@ export class LicenseStore {
|
|
|
1035
1061
|
}
|
|
1036
1062
|
else {
|
|
1037
1063
|
user.lastLoginAt = now;
|
|
1064
|
+
if (cleanProvider && cleanProvider !== 'email') user.authProvider = cleanProvider;
|
|
1038
1065
|
if (name) user.name = String(name).trim();
|
|
1039
1066
|
if (position) user.position = String(position).trim();
|
|
1040
1067
|
if (companyName) user.companyName = String(companyName).trim();
|
|
@@ -1057,11 +1084,11 @@ export class LicenseStore {
|
|
|
1057
1084
|
}
|
|
1058
1085
|
}
|
|
1059
1086
|
|
|
1060
|
-
if (ofertaAccepted && !user.ofertaAccepted) {
|
|
1087
|
+
if (ofertaAccepted !== false && !user.ofertaAccepted) {
|
|
1061
1088
|
user.ofertaAccepted = true;
|
|
1062
1089
|
user.ofertaAcceptedAt = now;
|
|
1063
1090
|
user.ofertaVersion = PUBLIC_OFERTA.version;
|
|
1064
|
-
user.ofertaSignerName = String(name || user.name || user.companyName).trim();
|
|
1091
|
+
user.ofertaSignerName = String(name || user.name || user.companyName || user.email).trim();
|
|
1065
1092
|
user.ofertaSignatureId = 'PEP-' + crypto.randomBytes(4).toString('hex').toUpperCase();
|
|
1066
1093
|
}
|
|
1067
1094
|
|
|
@@ -1298,6 +1325,7 @@ export class LicenseStore {
|
|
|
1298
1325
|
*/
|
|
1299
1326
|
reserveFranchiseSlot({
|
|
1300
1327
|
regionCode = 'RU-SVE',
|
|
1328
|
+
ownerAccountId = '',
|
|
1301
1329
|
companyName = '',
|
|
1302
1330
|
inn = '',
|
|
1303
1331
|
email = '',
|
|
@@ -1305,8 +1333,7 @@ export class LicenseStore {
|
|
|
1305
1333
|
contactPerson = '',
|
|
1306
1334
|
returnUrl = '',
|
|
1307
1335
|
} = {}) {
|
|
1308
|
-
|
|
1309
|
-
throw new Error('Укажите наименование компании для резервирования региональной франшизы');
|
|
1336
|
+
const effCompanyName = String(companyName || email || 'Партнёр ODANT-WORK').trim();
|
|
1310
1337
|
|
|
1311
1338
|
const quotaStatus = this.getRegionPartnerQuotaStatus(regionCode);
|
|
1312
1339
|
if (quotaStatus.vacantFranchises <= 0) {
|
|
@@ -1320,14 +1347,17 @@ export class LicenseStore {
|
|
|
1320
1347
|
const reservationId = 'RES-' + quotaStatus.regionCode.replace(/^RU-/, '') + '-' + crypto.randomBytes(3).toString('hex').toUpperCase();
|
|
1321
1348
|
const depositRub = FRANCHISE_PROGRAM.reservationDepositRub;
|
|
1322
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);
|
|
1323
1352
|
|
|
1324
1353
|
const reservation = {
|
|
1325
1354
|
reservationId,
|
|
1326
1355
|
orderId,
|
|
1356
|
+
ownerAccountId: String(ownerAccountId || '').trim(),
|
|
1327
1357
|
regionCode: quotaStatus.regionCode,
|
|
1328
1358
|
regionName: quotaStatus.regionName,
|
|
1329
1359
|
district: quotaStatus.district,
|
|
1330
|
-
companyName:
|
|
1360
|
+
companyName: effCompanyName,
|
|
1331
1361
|
inn: String(inn || '').trim(),
|
|
1332
1362
|
email: String(email || 'franchise@odant.org').trim(),
|
|
1333
1363
|
phone: String(phone || '').trim(),
|
|
@@ -1345,6 +1375,7 @@ export class LicenseStore {
|
|
|
1345
1375
|
|
|
1346
1376
|
const order = {
|
|
1347
1377
|
orderId,
|
|
1378
|
+
ownerAccountId: String(ownerAccountId || '').trim(),
|
|
1348
1379
|
kind: 'franchise_reservation',
|
|
1349
1380
|
reservationId,
|
|
1350
1381
|
licenseId: reservationId,
|
|
@@ -1355,6 +1386,9 @@ export class LicenseStore {
|
|
|
1355
1386
|
years: 1,
|
|
1356
1387
|
amountRub: depositRub,
|
|
1357
1388
|
franchiseFeeRub: quotaStatus.franchiseFeeRub,
|
|
1389
|
+
invoiceNumber,
|
|
1390
|
+
contractNumber,
|
|
1391
|
+
invoiceUrl: `/api/orders/${encodeURIComponent(orderId)}/invoice`,
|
|
1358
1392
|
items: [
|
|
1359
1393
|
{
|
|
1360
1394
|
title: `Невозвратный залог за резервирование региональной франшизы ODANT-WORK (${quotaStatus.regionName}, паушальный взнос ${quotaStatus.franchiseFeeRub.toLocaleString('ru-RU')} ₽, полная оплата в течение 1 года)`,
|
|
@@ -1388,6 +1422,67 @@ export class LicenseStore {
|
|
|
1388
1422
|
};
|
|
1389
1423
|
}
|
|
1390
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
|
+
|
|
1391
1486
|
/**
|
|
1392
1487
|
* Зарегистрировать регионального партнёра-франчайзи с контролем региональной квоты (по населению)
|
|
1393
1488
|
* и расчётом фиксированного паушального взноса (пропорционально экономике региона).
|