1// LICENSE_CODE ZON 2'use strict'; /*jslint node:true, browser:true, es6:true*/ 3(function(){ 4let define; 5let is_node = typeof module=='object' && module.exports; 6if (!is_node) 7 define = self.define; 8else 9 define = require('../../../util/require_node.js').define(module, '../'); 10define(['/util/date.js', '/util/country.js', '/util/util.js'], 11 (date, zcountry, zutil)=>{ 12const E = {t: {}}; 13const {ms} = date; 14 15E.desktop_gateways = ['paypal', 'avangate', 'bluesnap', 'paypal2', 16 'coupon', 'stripe', 'adyen']; 17 18E.mobile_gateways = ['android', 'apple']; 19 20E.gateways = E.desktop_gateways.concat(E.mobile_gateways); 21 22// extra days for a case when automatic payment comes lately 23E.grace = 1; 24 25// list of periods that we have or had 26E.periods = ['1 D', '1 W', '1 M', '3 M', '6 M', '1 Y', '2 Y', '3 Y']; 27 28E.period_unit_to_ms = {D: ms.DAY, W: 7*ms.DAY, M: ms.MONTH, Y: ms.YEAR}; 29 30E.period_unit_names = {D: 'day', W: 'week', M: 'month', Y: 'year'}; 31E.type2name= { 32 plus: 'premium', 33 ultra: 'ultra', 34}; 35 36E.get_country_code = membership=>{ 37 if (!membership) 38 return; 39 const country_name = membership.geo_country || membership.country; 40 return zcountry.mixed2code(country_name) || country_name; 41}; 42 43E.get_gateways = type=>{ 44 if (!type) 45 return []; 46 switch (type) 47 { 48 case 'desktop': return E.desktop_gateways; 49 case 'mobile': return E.mobile_gateways; 50 case 'paypal': return ['paypal', 'paypal2']; 51 } 52 return E.gateways.includes(type) ? [type] : []; 53}; 54 55E.get_period_ms = period=>{ 56 const {count, unit} = E.parse_period(period) || {}; 57 return count*E.period_unit_to_ms[unit]; 58}; 59 60E.get_offsets = d=>{ 61 const offsets = {}; 62 for (const p of E.periods) 63 offsets[p] = E.offset_by_period(d, p, -1); 64 return offsets; 65}; 66 67E.offset_by_period = (d, period, factor=1)=>{ 68 d = date(d, true); 69 const {count, unit} = E.parse_period(period) || {}; 70 switch (unit) 71 { 72 case 'D': 73 d.setUTCDate(d.getUTCDate()+count*factor); 74 return d; 75 case 'W': 76 d.setUTCDate(d.getUTCDate()+7*count*factor); 77 return d; 78 case 'M': 79 { 80 const day = d.getUTCDate(); 81 d.setUTCMonth(d.getUTCMonth()+count*factor); 82 // fix for 31-Jan to be 28-Feb 83 if (d.getUTCDate()!=day) 84 d.setUTCDate(0); 85 return d; 86 } 87 case 'Y': 88 d.setUTCFullYear(d.getUTCFullYear()+count*factor); 89 return d; 90 } 91}; 92 93const get_end_by_period = (m, opt)=>{ 94 opt = opt||{}; 95 const test_iap = m.test && is_app_store_iap(m); 96 let end = m.period && !test_iap 97 ? E.offset_by_period(opt.start||m.start, m.period) 98 : m.end || m.trial_end || m.grace_end; 99 if (!end) 100 { 101 throw new Error('Unexpected period: '+m.period +' ('+JSON.stringify(m) 102 +')'); 103 } 104 end = date(end); 105 if (!opt.disable_grace) 106 { 107 if (test_iap) 108 end.setUTCMinutes(end.getUTCMinutes()+E.grace); 109 else 110 end.setUTCDate(end.getUTCDate()+E.grace); 111 } 112 return end; 113}; 114 115E.get_length_by_period = period=>{ 116 const start = Date.now(); 117 return get_end_by_period({period}, {start})-start; 118}; 119 120E.get_dur = m=>get_end_by_period(m, {disable_grace: true})-date(m.start); 121 122E.get_end_by_period = get_end_by_period; 123 124E.get_end_date = m=>{ 125 const ends = []; 126 if (m && m.trial_end) 127 ends.push(date(m.trial_end)); 128 if (m && m.grace_end) 129 ends.push(date(m.grace_end)); 130 if (m && m.end) 131 ends.push(date(m.end)); 132 if (m && m.start && m.period && (!m.cancelled || !m.end) && !E.is_trial(m)) 133 { 134 const _end = get_end_by_period(m); 135 if (_end) 136 ends.push(_end); 137 } 138 if (!ends.length) 139 return null; 140 const res = date(Math.max.apply(null, ends)); 141 return res; 142}; 143 144E.is_active = (m, _now)=>{ 145 const now = _now||Date.now(); 146 if (!m) 147 return false; 148 if (m.trial_end && now<=date(m.trial_end)) 149 return true; 150 if (m.grace_end && now<=date(m.grace_end)) 151 return true; 152 if (m.end && now<=date(m.end)) 153 return true; 154 if (m.start && m.period && now<=get_end_by_period(m) && !E.is_trial(m)) 155 return !m.cancelled || !m.end; 156 return false;
157}; 158 159E.is_ultra = m=>E.is_active(m) && m && m.type=='ultra'; 160 161E.is_lifetime = m=>m && m.period=='100 Y'; 162 163E.is_in_trial = m=>E.is_trial(m) && Date.now()<date(m.trial_end||m.end); 164 165E.is_trial = (m, type)=>!!(m && (m.trial_end 166 && (!type || type==(m.type||'start')) || m.plan=='trial' && m.end)); 167 168E.is_in_grace = m=>E.is_grace(m) && Date.now()<date(m.grace_end); 169 170E.is_grace = (m, type)=>!!m && !!m.grace_end 171 && (!type || type==(m.type||'start')); 172 173E.is_manual = m=>!!m && m.plan=='manual'; 174 175E.had_premium = history=>!!history && history.some(m=>!E.is_trial(m)); 176 177E.had_trial = (history, type)=>!!history 178 && history.some(h=>E.is_trial(h, type)); 179 180E.had_grace = (history, type)=>!!history 181 && history.some(h=>E.is_grace(h, type)); 182 183E.trial_forbidden = (m, history, type)=>{ 184 if (E.is_in_trial(m)) 185 return 'trial exists'; 186 if (E.is_trial(m, type)) 187 return 'trial expired'; 188 if (E.is_active(m)) 189 return 'already premium'; 190 if (E.had_premium(history)) 191 return 'had premium'; 192 if (E.had_trial(history, type)) 193 return 'had trial'; 194 return false; 195}; 196 197E.grace_period_forbidden = (m, history, type)=>{ 198 if (E.is_in_grace(m)) 199 return 'grace exists'; 200 if (E.is_grace(m, type)) 201 return 'grace expired'; 202 if (E.is_active(m)) 203 return 'already premium'; 204 if (E.had_grace(history, type)) 205 return 'had grace'; 206 return false; 207}; 208 209E.is_paid = m=>!!m && !!m.gateway; 210 211E.is_paid_amount = m=>E.is_paid(m) && (m.amount || m.amount_usd); 212 213E.is_expired = m=>{ 214 const end_date = E.get_end_date(m); 215 return !!end_date && Date.now()>end_date; 216}; 217 218E.classify = m=>{ 219 if (E.is_active(m)) 220 return E.is_in_trial(m) ? 'trial' : 'premium'; 221 if (E.is_expired(m)) 222 return E.is_trial(m) ? 'trial_expired' : 'premium_expired'; 223 return 'free'; 224}; 225 226E.gen_email_hash = (email, md5)=>md5('hola unsubscribe '+email).substr(0, 8); 227 228E.fmt_date = (d, fmt)=>d ? date.strftime(fmt || '%o %B %Y', date(d)) : '-'; 229 230// gateway does not exist in ios trial 231E.is_ios = m=>m.gateway=='apple' || m.product=='ios'; 232 233E.is_mobile = m=>E.is_ios(m) 234 || !!m.gateway && !E.desktop_gateways.includes(m.gateway); 235 236E.is_desktop = m=>E.desktop_gateways.includes(m.gateway); 237 238E.get_plan_short_name = period=>{ 239 const {count, unit} = E.parse_period(period) || {}; 240 const period_name = E.period_unit_names[unit]; 241 let short_name = {'1 D': 'Daily', '1 W': 'Weekly', '1 M': 'Monthly', 242 '1 Y': 'Yearly'}[period]; 243 if (!short_name) 244 { 245 short_name = count+' '+period_name[0].toUpperCase() 246 +period_name.substr(1); 247 if (count>1) 248 short_name += 's'; 249 } 250 return short_name; 251}; 252 253E.get_period_with_price = m=>{ 254 if (m.plan=='signup_bonus') 255 return 'Signup bonus'; 256 if (m.period=='100 Y') 257 return 'Lifetime'; 258 const {count, unit} = E.parse_period(m.period) || {}; 259 if (m.gateway=='coupon' && count) 260 return count+'-'+E.period_unit_names[unit]; 261 const end = E.get_end_date(m); 262 if (m.cancelled) 263 return 'Till '+E.fmt_date(end); 264 const show_price = name=>{ 265 if (!m.amount) 266 return name; 267 const price = m.amount; 268 const currency = m.currency; 269 const vat = m.vat; 270 const p = +(price/count).toFixed(2); 271 const price_string = zcountry.format_price(currency, p); 272 return name+', '+price_string+'/'+E.period_unit_names[unit] 273 +(vat && vat.percent ? 274 ' (includes VAT '+vat.percent+'% ' 275 +zcountry.format_price(currency, vat.amount)+')' : ''); 276 }; 277 switch (unit) 278 { 279 case 'W': 280 if (count==1) 281 return show_price('Weekly'); 282 return show_price(count+'-week'); 283 case 'M': 284 if (count==1) 285 return show_price('Monthly'); 286 if (count==6) 287 return show_price('Half-yearly'); 288 return show_price(count+'-month'); 289 case 'Y': 290 if (count==1) 291 return show_price('Yearly'); 292 return show_price(count+'-year'); 293 case 'D': 294 return show_price(count+'-day'); 295 } 296 if (!m.end) 297 return m.grace_end ? 'updating' : '-'; 298 return (m.gateway ? 'Till ' : 'Manual till ') +E.fmt_date(end); 299}; 300 301E.get_per = (period, full)=>{ 302 const {count, unit} = E.parse_period(period) || {}; 303 const name = E.period_unit_names[unit]; 304 return count==1 ? (full ? count+' ' : '')+name : count+' '+name+'s'; 305}; 306 307E.sum_period = (p1, p2)=>{ 308 const {count: c1, unit: u1} = E.parse_period(p1) || {}; 309 const {count: c2, unit: u2} = E.parse_period(p2) || {}; 310 if (u1==u2) 311 return `${c1+c2} ${u1}`;
312 let c_min, c_max, u_min, u_max; 313 const ums = E.period_unit_to_ms; 314 if (ums[u1]>ums[u2]) 315 { 316 c_max = c1; 317 u_max = u1; 318 c_min = c2; 319 u_min = u2; 320 } 321 else 322 { 323 c_max = c2; 324 u_max = u2; 325 c_min = c1; 326 u_min = u1; 327 } 328 if (u_min=='D') 329 return c_min+c_max*ums[u_max]/ums.D+' D'; 330 if (u_min=='W') 331 return c_min*7+c_max*ums[u_max]/ums.D+' D'; 332 if (u_min=='M') 333 return c_min+c_max*12+' M'; 334}; 335 336E.period_start_str = m=>E.fmt_date(m.start); 337 338E.period_end_str = (m, fmt)=>{ 339 if (E.is_in_trial(m)) 340 return 'Trial valid until '+E.fmt_date(m.trial_end); 341 if (m.next_renewal_date) 342 return E.fmt_date(m.next_renewal_date); 343 if (m.cancelled || !m.period) 344 return m.grace_end ? 'updating' : '-'; 345 return E.fmt_date(m.end || E.offset_by_period(m.start, m.period), fmt); 346}; 347 348// ios app is not associated with App Store until after trial is expired 349E.is_unassoc_trial = m=>E.is_trial(m) && E.is_ios(m); 350 351E.is_bonus = m=>m && m.plan=='signup_bonus'; 352 353E.payment_label = m=>{ 354 if (E.is_unassoc_trial(m) || E.is_bonus(m)) 355 return '-'; 356 switch (m.gateway) 357 { 358 case 'paypal': 359 case 'paypal2': return 'PayPal'; 360 case 'coupon': return 'Coupon'; 361 case 'avangate': 362 case 'stripe': 363 case 'bluesnap': return 'Credit card'; 364 case 'apple': return 'App Store'; 365 case 'android': return 'Google Play'; 366 default: return m.grace_end ? 'updating' 367 : m.plan=='manual' ? 'Other' : 'Google Play'; 368 } 369}; 370 371E.gateway_id_path = gateway=>{ 372 switch (gateway) 373 { 374 case 'paypal': 375 case 'paypal2': 376 return 'paypal.subscr_id'; 377 case 'coupon': return 'coupon.code'; 378 case 'bluesnap': return 'bluesnap.subscr_id'; 379 case 'stripe': return 'stripe.subscr_id'; 380 case 'apple': return 'apple_ios.original_transaction_id'; 381 case 'android': return 'google_apk.purchase_token'; 382 case 'avangate': return 'avangate.subscr_ref'; 383 case 'adyen': return 'adyen.subscr_id'; 384 } 385}; 386 387E.get_paypal_id = m=>zutil.get(m, 'paypal.subscr_id') 388 || zutil.get(m, 'paypal2.subscr_id') || zutil.get(m, 'paypal.txn_id'); 389 390E.parse_id = (subscr_id, gateway)=>{ 391 if (gateway=='bluesnap' && subscr_id) 392 return +subscr_id; 393 return subscr_id; 394}; 395 396E.get_id = (m, gateway)=>{ 397 if (!m) 398 return; 399 if (m && m.plan=='manual') 400 return +date(m.start); 401 gateway = gateway||m.gateway; 402 switch (gateway) 403 { 404 case 'paypal': 405 case 'paypal2': 406 return E.get_paypal_id(m); 407 case 'bluesnap_gpay': 408 case 'bluesnap_apm': 409 case 'bluesnap_applepay': 410 case 'bluesnap': 411 return m.bluesnap && (m.bluesnap.subscr_id || m.bluesnap.ref); 412 case 'apple': 413 return zutil.get(m, 'apple_ios.original_transaction_id') 414 || zutil.get(m, 'apple_ios.transaction_id'); 415 case 'coupon': 416 case 'android': 417 case 'avangate': 418 case 'stripe': 419 case 'adyen': 420 return zutil.get(m, E.gateway_id_path(gateway)); 421 } 422}; 423 424E.get_gateway_name = (m, gateway)=>{ 425 gateway = gateway || m && m.gateway; 426 switch (gateway) 427 { 428 case 'bluesnap': 429 if (zutil.get(m, 'bluesnap.gpay')) 430 gateway = 'bluesnap_gpay'; 431 else if (zutil.get(m, 'bluesnap.applepay')) 432 gateway = 'bluesnap_applepay'; 433 else if (!zutil.get(m, 'bluesnap.subscr_id') 434 && zutil.get(m, 'bluesnap.ref')) 435 { 436 gateway = 'bluesnap_apm'; 437 } 438 break; 439 case 'paypal': 440 case 'paypal2': 441 gateway = 'paypal'; 442 break; 443 case 'avangate': 444 case 'stripe': 445 case 'apple': 446 case 'android': 447 case 'coupon': 448 case 'bluesnap_gpay': 449 case 'bluesnap_apm': 450 case 'bluesnap_applepay': 451 case 'adyen': 452 break; 453 default: 454 if (m && m.plan=='trial') 455 gateway = 'trial'; 456 else if (m && m.plan=='grace') 457 gateway = 'grace'; 458 else if (m && m.plan=='manual') 459 gateway = 'manual'; 460 else 461 gateway = 'unknown'; 462 } 463 return gateway; 464}; 465 466E.get_full_id = (m, gateway, id)=>{ 467 id = id||E.get_id(m); 468 if (!id) 469 return; 470 return E.get_gateway_name(m, gateway)+'-'+id; 471}; 472 473E.is_cancellable_mobile = m=>E.is_mobile(m) && !E.is_unassoc_trial(m) 474 && (m.cancelled || m.cancellation_reason); 475 476E.is_cancellable = m=>!m.cancelled && !E.is_cancellable_mobile(m) 477 && !E.is_unassoc_trial(m) && !m.grace_end && !E.is_bonus(m); 478 479E.get_plan_currency = (plan, currency)=>{ 480 if (!currency || !plan) 481 return 'USD'; 482 currency = currency.toUpperCase(); 483 if (currency=='USD') 484 return 'USD'; 485 if (!plan.currency || !plan.currency[currency.toLowerCase()]) 486 return 'USD'; 487 return currency; 488}; 489 490E.get_price_plan = (plan, currency)=>{ 491 currency = (''+currency).toLowerCase(); 492 return currency=='usd' || !plan.currency || !plan.currency[currency] ? plan 493 : plan.currency[currency]; 494}; 495 496E.is_better = (m1, m2)=>{ 497 if (!m1) 498 return false;
499 if (!m2) 500 return true; 501 if (m1.type=='ultra' && m2.type!='ultra') 502 return true; 503 if (m2.type=='ultra' && m1.type!='ultra') 504 return false; 505 if (!m1.period && m1.plan!='manual' && m2.period) 506 return false; 507 if (!m2.period && m2.plan!='manual' && m1.period) 508 return true; 509 const start = Date.now(); 510 const end1 = get_end_by_period(m1, {start, disable_grace: true}); 511 const end2 = get_end_by_period(m2, {start, disable_grace: true}); 512 return end1>end2; 513}; 514 515E.is_more_actual = (m1, m2)=>{ 516 if (!m1) 517 return false; 518 if (!m2) 519 return true; 520 if (!m1.period && m1.plan!='manual') 521 return false; 522 if (!m2.period && m2.plan!='manual') 523 return true; 524 const end1 = E.get_end_date(m1); 525 const end2 = E.get_end_date(m2); 526 return end1>end2; 527}; 528 529E.find_last = (m, history, subscr_id)=>{ 530 if (m && E.get_id(m)==subscr_id) 531 return {m}; 532 if (!Array.isArray(history)) 533 return {}; 534 for (let i=history.length-1; i>=0; i--) 535 { 536 if (E.get_id(history[i])==subscr_id) 537 return {m: history[i], index: i}; 538 } 539 return {}; 540}; 541 542const is_app_store_iap = m=>{ 543 return (zutil.get(m, 'apple_ios.bundle_id')||'') 544 .startsWith('org.hola.hola'); 545}; 546 547E.t.get_exp_date_by_gateway = (m, gateway)=>{ 548 gateway = gateway||m.gateway; 549 switch (gateway) 550 { 551 // XXX vitaly: sometimes avangate sends outdated expiration_date, 552 // so we can't rely on it. Example: google/106641825611906290005 553 // case 'avangate': return zutil.get(m, 'avangate.expiration_date'); 554 case 'bluesnap': return zutil.get(m, 'bluesnap.exp_date'); 555 case 'stripe': return zutil.get(m, 'stripe.exp_date'); 556 case 'android': return zutil.get(m, 'google_apk.expiry'); 557 } 558 // XXX mikita: all appstore payments expiry already casted to end 559 if (E.is_ios(m)) 560 return is_app_store_iap(m) && m.test ? m.end : undefined; 561}; 562 563const get_seq_id_by_exp = (exp, m)=>{ 564 const period = m.period.split(' '); 565 // XXX mikita: sandbox payment periods are drastically reduced to minutes 566 if (is_app_store_iap(m) && m.test) 567 return +date.align(exp, 'MIN'); 568 const align = {D: 'DAY', W: 'WEEK', M: 'MONTH', Y: 'YEAR'}[period[1]]; 569 return +date.align(exp, align); 570}; 571 572const get_seq_data = m=>{ 573 if (!m || !m.period) 574 return; 575 const exp_date = E.t.get_exp_date_by_gateway(m); 576 let exp = date(exp_date || E.offset_by_period(m.start, m.period)); 577 if (m.end && date(m.end)>exp) 578 exp = date(m.end); 579 return {seq_id: get_seq_id_by_exp(exp, m), exp, 580 persistent: m.persistent || !!exp_date}; 581}; 582 583E.get_seq_id = m=>{ 584 const res = get_seq_data(m); 585 return res && res.seq_id; 586}; 587 588E.make_seqs = memberships=>{ 589 const ids = new Set(); 590 for (const m of memberships) 591 { 592 const id = E.get_id(m); 593 if (id) 594 ids.add(id); 595 } 596 const res = {}; 597 for (const id of ids) 598 res[id] = E.make_seq(memberships, id); 599 return res; 600}; 601 602E.make_seq = (memberships, subscr_id)=>{ 603 const list = []; 604 for (const m of memberships) 605 { 606 if (!E.is_paid_amount(m) || {trial: 1, grace: 1}[m.plan]) 607 continue; 608 if (subscr_id && E.get_id(m)!=subscr_id) 609 continue; 610 list.push(m); 611 } 612 list.sort((m1, m2)=>{ 613 if (date(m1.start)<date(m2.start)) 614 return -1; 615 if (date(m1.start)>date(m2.start)) 616 return 1; 617 return 0; 618 }); 619 let seq = []; 620 for (let i=0; i<list.length; i++) 621 { 622 const m = list[i]; 623 const start = date(m.start); 624 let {seq_id, exp, persistent} = get_seq_data(m)||{}; 625 const last = seq[seq.length-1]; 626 // skip with same start date 627 if (last && !(start-date(last.m.start))) 628 { 629 // prefer persistent over not persistent 630 if (persistent && !last.persistent) 631 seq[seq.length-1] = {m, seq_id, exp, persistent}; 632 seq[seq.length-1].duplicate = true; 633 continue; 634 } 635 const end = m.end && date(m.end); 636 // skip short membership inside previous membership 637 if (last && end && start<end && end<last.exp) 638 { 639 last.duplicate = true; 640 continue; 641 } 642 const next_seq_id = last && +E.offset_by_period(last.seq_id, m.period); 643 // restore interrupted sequence by shifting last expiration date 644 // if sequence is interrupted for no more than one period 645 if (next_seq_id && seq_id>next_seq_id 646 && seq_id<=+E.offset_by_period(next_seq_id, m.period)) 647 { 648 exp = E.offset_by_period(last.exp, m.period); 649 seq_id = next_seq_id; 650 } 651 if (last && last.seq_id>=seq_id) 652 { 653 // skip real duplicates 654 if (last.seq_id==seq_id 655 && (!persistent || persistent && last.persistent)) 656 { 657 last.duplicate = true; 658 continue; 659 } 660 // restore interrupted sequence by review and shifting all previous 661 const new_seq = [{m, seq_id, exp, persistent}]; 662 for (let j=seq.length-1; j>=0; j--) 663 { 664 const item = seq[j]; 665 if (!item.persistent) 666 { 667 item.exp = E.offset_by_period(new_seq[0].exp, m.period, 668 -1); 669 item.seq_id = get_seq_id_by_exp(item.exp, m); 670 } 671 // skip duplicates in new sequence 672 if (item.seq_id>=new_seq[0].seq_id 673 || item.exp<date(item.m.start)) 674 { 675 // prefer persistent over not persistent 676 if (item.persistent) 677 new_seq[0] = item; 678 new_seq[0].duplicate = true; 679 continue; 680 } 681 new_seq.unshift(item); 682 } 683 seq = new_seq; 684 continue; 685 } 686 seq.push({m, seq_id, exp, persistent}); 687 } 688 return seq; 689}; 690 691const omit_meta = o=>!o ? null : zutil.omit(o, ['actual_from', 'actual_to', 692 'update_res', 'reported', 'amount_usd', 'payment_intent_id', 'is_new', 693 'pending_emails']); 694 695E.parse_period = period=>{ 696 const m = (''+period).match(/^(\d+)\s?([dwmy])$/i); 697 if (!m) 698 return; 699 return {count: +m[1], unit: m[2].toUpperCase()}; 700}; 701 702E.is_duplicate = (m1, m2)=>{ 703 let omitted_m1 = omit_meta(m1); 704 let omitted_m2 = omit_meta(m2); 705 if (m1 && m2 && Math.abs(m2.start-m1.start<date.ms.DAY)) 706 { 707 omitted_m1 = zutil.omit(omitted_m1, ['start']); 708 omitted_m2 = zutil.omit(omitted_m2, ['start']); 709 } 710 if (m1 && m2 && Math.abs(m2.end-m1.end)<date.ms.DAY) 711 { 712 omitted_m1 = zutil.omit(omitted_m1, ['end']); 713 omitted_m2 = zutil.omit(omitted_m2, ['end']); 714 } 715 return zutil.equal_deep(omitted_m1, omitted_m2); 716}; 717 718E.get_hola_pro_product_by_subscription = (config, plan_id)=>{ 719 const migration_rule = config.migration_rule ? 720 config.migration_rule[plan_id] : ''; 721 const prod = config.plans_config[migration_rule || plan_id]; 722 if (!prod) 723 return null; 724 return { 725 id: prod.type, 726 period: prod.period, 727 limit: prod.limit, 728 ips_amount: prod.ips_amount, 729 quantity: prod.unit=='ip' ? prod.ips_amount : prod.limit, 730 plan_id: plan_id, 731 unit: prod.unit || 'gb', 732 }; 733}; 734 735return E; }); }());
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.