PageSourceSearch

https://cdn4.hola.org/svc/account/pub/membership.js

js hola.org collected 2026-09-24 10:17:45 UTC 21,488 bytes, 735 lines download raw bytes

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.