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/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 = false,
998
+ ofertaAccepted = true,
980
999
  hubUrl = 'https://work.odant.org',
981
1000
  } = {}) {
982
- const cleanEmail = String(email || '').trim().toLowerCase();
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('Укажите корректный корпоративный email для регистрации в личном кабинете ODANT-WORK');
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 || 'Организация ODANT-WORK',
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 || lic.customer?.name || 'Организация ODANT-WORK').trim(),
1018
- inn: String(inn || lic.customer?.inn || '').trim(),
1019
- phone: String(phone || lic.customer?.phone || '').trim(),
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
- if (!companyName)
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: String(companyName).trim(),
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
  * и расчётом фиксированного паушального взноса (пропорционально экономике региона).