<commit_before>import { Plan, PlanIDs, MaxKeys, Organization } from '../interfaces';
import { PLAN_SERVICES, PLANS, CYCLE, MAX_DOMAIN_PLUS_ADDON, ADDON_NAMES } from '../constants';

import { hasBit } from './bitset';
import { toMap, omit } from './object';

const { PLUS, VPNPLUS, VPNBASIC, VISIONARY, PROFESSIONAL } = PLANS;
const { VPN, SPACE, MEMBER, ADDRESS, DOMAIN } = ADDON_NAMES;

export const hasPlanIDs = (planIDs: PlanIDs) => Object.values(planIDs).some((quantity) => quantity > 0);

export const clearPlanIDs = (planIDs: PlanIDs) => {
    return Object.entries(planIDs).reduce<PlanIDs>((acc, [planID, quantity = 0]) => {
        if (quantity <= 0) {
            return acc;
        }
        acc[planID] = quantity;
        return acc;
    }, {});
};

export const removeService = (planIDs: PlanIDs, plans: Plan[], service: PLAN_SERVICES = PLAN_SERVICES.MAIL) => {
    const plansMap = toMap(plans);
    return Object.entries(planIDs).reduce<PlanIDs>((acc, [planID = '', quantity = 0]) => {
        const { Services } = plansMap[planID];

        if (hasBit(Services, service)) {
            return acc;
        }

        acc[planID] = quantity;
        return acc;
    }, {});
};

export const getHasPlanType = (planIDs: PlanIDs, plans: Plan[], planType: PLANS) => {
    const plan = plans.find(({ Name }) => Name === planType);
    return plan?.ID ? planIDs[plan.ID] >= 1 : false;
};

const getAddonQuantity = (plan: Plan | undefined, used = 0, key: MaxKeys, addon: Plan) => {
    const planKey = plan?.[key] || 0;

    if (!plan) {
        return 0;
    }

    if (used <= planKey) {
        return 0;
    }

    if (!addon) {
        return 0;
    }

    const addonKey = addon[key] || 0;
    return Math.ceil((used - planKey) / addonKey);
};

export const switchPlan = ({
    planIDs,
    plans,
    planID,
    service,
    organization,
}: {
    planIDs: PlanIDs;
    plans: Plan[];
    planID: string | undefined;
    service: PLAN_SERVICES;
    organization?: Organization;
}) => {
    // Handle FREE VPN and FREE Mail
    if (planID === undefined) {
        return removeService(planIDs, plans, service);
    }

    const plansMap = toMap(plans, 'Name');

    if (planID === plansMap[VISIONARY].ID) {
        return { [plansMap[VISIONARY].ID]: 1 };
    }

    const { UsedDomains = 0, UsedAddresses = 0, UsedSpace = 0, UsedVPN = 0, UsedMembers = 0 } = organization || {};
    const selectedPlan = plans.find(({ ID }) => ID === planID);

    const transferDomains = (from: PLANS, to: PLANS) => {
        const domains = planIDs[plansMap[DOMAIN].ID]
            ? plansMap[from].MaxDomains - plansMap[to].MaxDomains + planIDs[plansMap[DOMAIN].ID]
            : 0;
        if (domains < 0) {
            return 0;
        }
        const plusLimit = MAX_DOMAIN_PLUS_ADDON - plansMap[PLUS].MaxDomains;
        if (domains > plusLimit) {
            return plusLimit;
        }
        return domains;
    };

    if (plansMap[PLUS].ID === planID) {
        return clearPlanIDs({
            ...omit(planIDs, [
                plansMap[PLUS].ID,
                plansMap[PROFESSIONAL].ID,
                plansMap[VISIONARY].ID,
                plansMap[MEMBER].ID,
            ]),
            [plansMap[DOMAIN].ID]:
                transferDomains(PROFESSIONAL, PLUS) ||
                getAddonQuantity(selectedPlan, UsedDomains, 'MaxDomains', plansMap[DOMAIN]) ||
                0,
            [plansMap[ADDRESS].ID]:
                getAddonQuantity(selectedPlan, UsedAddresses, 'MaxAddresses', plansMap[ADDRESS]) || 0,
            [plansMap[SPACE].ID]: getAddonQuantity(selectedPlan, UsedSpace, 'MaxSpace', plansMap[SPACE]) || 0,
            [planID]: 1,
        });
    }

    if (plansMap[PROFESSIONAL].ID === planID) {
        const vpnAddons = getAddonQuantity(selectedPlan, UsedVPN, 'MaxVPN', plansMap[VPN]) || 0;

        return clearPlanIDs({
            ...omit(planIDs, [
                plansMap[PLUS].ID,
                plansMap[PROFESSIONAL].ID,
                plansMap[VISIONARY].ID,
                plansMap[ADDRESS].ID,
                plansMap[SPACE].ID,
            ]),
            [plansMap[MEMBER].ID]:
                Math.max(
                    getAddonQuantity(selectedPlan, UsedMembers, 'MaxMembers', plansMap[MEMBER]),
                    getAddonQuantity(selectedPlan, UsedAddresses, 'MaxAddresses', plansMap[MEMBER]),
                    getAddonQuantity(selectedPlan, UsedSpace, 'MaxSpace', plansMap[MEMBER])
                ) || 0,
            [plansMap[DOMAIN].ID]:
                transferDomains(PLUS, PROFESSIONAL) ||
                getAddonQuantity(selectedPlan, UsedDomains, 'MaxDomains', plansMap[DOMAIN]) ||
                0,
            ...(!planIDs[plansMap[VPNBASIC].ID]
                ? {
                      [plansMap[VPNPLUS].ID]: vpnAddons || planIDs[plansMap[VPNPLUS].ID] ? 1 : 0,
                      [plansMap[VPN].ID]: vpnAddons ? vpnAddons - plansMap[VPNPLUS].MaxVPN : 0,
                  }
                : undefined),
            [planID]: 1,
        });
    }

    if (plansMap[VPNBASIC].ID === planID) {
        return clearPlanIDs({
            ...omit(planIDs, [plansMap[VPNBASIC].ID, plansMap[VPNPLUS].ID, plansMap[VISIONARY].ID, plansMap[VPN].ID]),
            [planID]: 1,
        });
    }

    if (plansMap[VPNPLUS].ID === planID) {
        return clearPlanIDs({
            ...omit(planIDs, [plansMap[VPNBASIC].ID, plansMap[VPNPLUS].ID, plansMap[VISIONARY].ID]),
            [plansMap[VPN].ID]: planIDs[plansMap[PROFESSIONAL].ID]
                ? getAddonQuantity(selectedPlan, UsedVPN, 'MaxVPN', plansMap[VPN])
                : 0,
            [planID]: 1,<commit_msg>[CLIENT-4082] Handle BonusVPN in switchPlan<commit_after>import { Plan, PlanIDs, MaxKeys, Organization } from '../interfaces';
import { PLAN_SERVICES, PLANS, CYCLE, MAX_DOMAIN_PLUS_ADDON, ADDON_NAMES } from '../constants';

import { hasBit } from './bitset';
import { toMap, omit } from './object';

const { PLUS, VPNPLUS, VPNBASIC, VISIONARY, PROFESSIONAL } = PLANS;
const { VPN, SPACE, MEMBER, ADDRESS, DOMAIN } = ADDON_NAMES;

export const hasPlanIDs = (planIDs: PlanIDs) => Object.values(planIDs).some((quantity) => quantity > 0);

export const clearPlanIDs = (planIDs: PlanIDs) => {
    return Object.entries(planIDs).reduce<PlanIDs>((acc, [planID, quantity = 0]) => {
        if (quantity <= 0) {
            return acc;
        }
        acc[planID] = quantity;
        return acc;
    }, {});
};

export const removeService = (planIDs: PlanIDs, plans: Plan[], service: PLAN_SERVICES = PLAN_SERVICES.MAIL) => {
    const plansMap = toMap(plans);
    return Object.entries(planIDs).reduce<PlanIDs>((acc, [planID = '', quantity = 0]) => {
        const { Services } = plansMap[planID];

        if (hasBit(Services, service)) {
            return acc;
        }

        acc[planID] = quantity;
        return acc;
    }, {});
};

export const getHasPlanType = (planIDs: PlanIDs, plans: Plan[], planType: PLANS) => {
    const plan = plans.find(({ Name }) => Name === planType);
    return plan?.ID ? planIDs[plan.ID] >= 1 : false;
};

const getAddonQuantity = (plan: Plan | undefined, used = 0, key: MaxKeys, addon: Plan) => {
    const planKey = plan?.[key] || 0;

    if (!plan) {
        return 0;
    }

    if (used <= planKey) {
        return 0;
    }

    if (!addon) {
        return 0;
    }

    const addonKey = addon[key] || 0;
    return Math.ceil((used - planKey) / addonKey);
};

export const switchPlan = ({
    planIDs,
    plans,
    planID,
    service,
    organization,
}: {
    planIDs: PlanIDs;
    plans: Plan[];
    planID: string | undefined;
    service: PLAN_SERVICES;
    organization?: Organization;
}) => {
    // Handle FREE VPN and FREE Mail
    if (planID === undefined) {
        return removeService(planIDs, plans, service);
    }

    const plansMap = toMap(plans, 'Name');

    if (planID === plansMap[VISIONARY].ID) {
        return { [plansMap[VISIONARY].ID]: 1 };
    }

    const {
        UsedDomains = 0,
        UsedAddresses = 0,
        UsedSpace = 0,
        UsedVPN = 0,
        UsedMembers = 0,
        BonusVPN = 0,
    } = organization || {};
    const selectedPlan = plans.find(({ ID }) => ID === planID);

    const transferDomains = (from: PLANS, to: PLANS) => {
        const domains = planIDs[plansMap[DOMAIN].ID]
            ? plansMap[from].MaxDomains - plansMap[to].MaxDomains + planIDs[plansMap[DOMAIN].ID]
            : 0;
        if (domains < 0) {
            return 0;
        }
        const plusLimit = MAX_DOMAIN_PLUS_ADDON - plansMap[PLUS].MaxDomains;
        if (domains > plusLimit) {
            return plusLimit;
        }
        return domains;
    };

    if (plansMap[PLUS].ID === planID) {
        return clearPlanIDs({
            ...omit(planIDs, [
                plansMap[PLUS].ID,
                plansMap[PROFESSIONAL].ID,
                plansMap[VISIONARY].ID,
                plansMap[MEMBER].ID,
            ]),
            [plansMap[DOMAIN].ID]:
                transferDomains(PROFESSIONAL, PLUS) ||
                getAddonQuantity(selectedPlan, UsedDomains, 'MaxDomains', plansMap[DOMAIN]) ||
                0,
            [plansMap[ADDRESS].ID]:
                getAddonQuantity(selectedPlan, UsedAddresses, 'MaxAddresses', plansMap[ADDRESS]) || 0,
            [plansMap[SPACE].ID]: getAddonQuantity(selectedPlan, UsedSpace, 'MaxSpace', plansMap[SPACE]) || 0,
            [planID]: 1,
        });
    }

    if (plansMap[PROFESSIONAL].ID === planID) {
        const vpnAddons = getAddonQuantity(selectedPlan, UsedVPN, 'MaxVPN', plansMap[VPN]) || 0;

        return clearPlanIDs({
            ...omit(planIDs, [
                plansMap[PLUS].ID,
                plansMap[PROFESSIONAL].ID,
                plansMap[VISIONARY].ID,
                plansMap[ADDRESS].ID,
                plansMap[SPACE].ID,
            ]),
            [plansMap[MEMBER].ID]:
                Math.max(
                    getAddonQuantity(selectedPlan, UsedMembers, 'MaxMembers', plansMap[MEMBER]),
                    getAddonQuantity(selectedPlan, UsedAddresses, 'MaxAddresses', plansMap[MEMBER]),
                    getAddonQuantity(selectedPlan, UsedSpace, 'MaxSpace', plansMap[MEMBER])
                ) || 0,
            [plansMap[DOMAIN].ID]:
                transferDomains(PLUS, PROFESSIONAL) ||
                getAddonQuantity(selectedPlan, UsedDomains, 'MaxDomains', plansMap[DOMAIN]) ||
                0,
            ...(!planIDs[plansMap[VPNBASIC].ID]
                ? {
                      [plansMap[VPNPLUS].ID]: vpnAddons || planIDs[plansMap[VPNPLUS].ID] ? 1 : 0,
                      [plansMap[VPN].ID]: vpnAddons ? vpnAddons - plansMap[VPNPLUS].MaxVPN - BonusVPN : 0,
                  }
                : undefined),
            [planID]: 1,
        });
    }

    if (plansMap[VPNBASIC].ID === planID) {
        return clearPlanIDs({
            ...omit(planIDs, [plansMap[VPNBASIC].ID, plansMap[VPNPLUS].ID, plansMap[VISIONARY].ID, plansMap[VPN].ID]),
            [planID]: 1,
        });
    }

    if (plansMap[VPNPLUS].ID === planID) {
        return clearPlanIDs({
            ...omit(planIDs, [plansMap[VPNBASIC].ID, plansMap[VPNPLUS].ID, plansMap[VISIONARY].ID]),
            [plansMap[VPN].ID]: planIDs[plansMap[PROFESSIONAL].ID]
                ? getAddonQuantity(selectedPlan, UsedVPN, 'MaxVPN', plansMap[VPN])
                : 0,
            [planID]: 1,
{'fraction_non_alphanumeric': 0.1418954362650813, 'fraction_numerical': 0.006382234656408463, 'mean_word_length': 2.144310060472787, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 37, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2750263', 'n_tokens_mistral': 3824, 'n_tokens_neox': 3628, 'n_words': 720}