mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-10 06:39:01 +02:00
Merge pull request #4071 from overleaf/ab-subscription-decaf-cleanup
Subscription controller decaf cleanup GitOrigin-RevId: 79b8adfabe30e4557a95b1aad71a5162e6f42cce
This commit is contained in:
committed by
Copybot
parent
b93761f275
commit
18d62dcee9
@@ -10,6 +10,7 @@ const sanitizeHtml = require('sanitize-html')
|
||||
const _ = require('underscore')
|
||||
const async = require('async')
|
||||
const SubscriptionHelper = require('./SubscriptionHelper')
|
||||
const { promisify } = require('../../util/promises')
|
||||
|
||||
function buildHostedLink(recurlySubscription, type) {
|
||||
const recurlySubdomain = Settings.apis.recurly.subdomain
|
||||
@@ -29,311 +30,312 @@ function buildHostedLink(recurlySubscription, type) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildUsersSubscriptionViewModel(user, callback) {
|
||||
async.auto(
|
||||
{
|
||||
personalSubscription(cb) {
|
||||
SubscriptionLocator.getUsersSubscription(user, cb)
|
||||
},
|
||||
recurlySubscription: [
|
||||
'personalSubscription',
|
||||
(cb, { personalSubscription }) => {
|
||||
if (
|
||||
personalSubscription == null ||
|
||||
personalSubscription.recurlySubscription_id == null ||
|
||||
personalSubscription.recurlySubscription_id === ''
|
||||
) {
|
||||
return cb(null, null)
|
||||
}
|
||||
RecurlyWrapper.getSubscription(
|
||||
personalSubscription.recurlySubscription_id,
|
||||
{ includeAccount: true },
|
||||
cb
|
||||
)
|
||||
},
|
||||
],
|
||||
recurlyCoupons: [
|
||||
'recurlySubscription',
|
||||
(cb, { recurlySubscription }) => {
|
||||
if (!recurlySubscription) {
|
||||
return cb(null, null)
|
||||
}
|
||||
const accountId = recurlySubscription.account.account_code
|
||||
RecurlyWrapper.getAccountActiveCoupons(accountId, cb)
|
||||
},
|
||||
],
|
||||
plan: [
|
||||
'personalSubscription',
|
||||
(cb, { personalSubscription }) => {
|
||||
if (personalSubscription == null) {
|
||||
return cb()
|
||||
}
|
||||
const plan = PlansLocator.findLocalPlanInSettings(
|
||||
personalSubscription.planCode
|
||||
)
|
||||
if (plan == null) {
|
||||
return cb(
|
||||
new Error(
|
||||
`No plan found for planCode '${personalSubscription.planCode}'`
|
||||
)
|
||||
)
|
||||
}
|
||||
cb(null, plan)
|
||||
},
|
||||
],
|
||||
memberGroupSubscriptions(cb) {
|
||||
SubscriptionLocator.getMemberSubscriptions(user, cb)
|
||||
},
|
||||
managedGroupSubscriptions(cb) {
|
||||
SubscriptionLocator.getManagedGroupSubscriptions(user, cb)
|
||||
},
|
||||
confirmedMemberAffiliations(cb) {
|
||||
InstitutionsGetter.getConfirmedAffiliations(user._id, cb)
|
||||
},
|
||||
managedInstitutions(cb) {
|
||||
InstitutionsGetter.getManagedInstitutions(user._id, cb)
|
||||
},
|
||||
managedPublishers(cb) {
|
||||
PublishersGetter.getManagedPublishers(user._id, cb)
|
||||
},
|
||||
v1SubscriptionStatus(cb) {
|
||||
V1SubscriptionManager.getSubscriptionStatusFromV1(
|
||||
user._id,
|
||||
(error, status, v1Id) => {
|
||||
if (error) {
|
||||
return cb(error)
|
||||
}
|
||||
cb(null, status)
|
||||
}
|
||||
function buildUsersSubscriptionViewModel(user, callback) {
|
||||
async.auto(
|
||||
{
|
||||
personalSubscription(cb) {
|
||||
SubscriptionLocator.getUsersSubscription(user, cb)
|
||||
},
|
||||
recurlySubscription: [
|
||||
'personalSubscription',
|
||||
(cb, { personalSubscription }) => {
|
||||
if (
|
||||
personalSubscription == null ||
|
||||
personalSubscription.recurlySubscription_id == null ||
|
||||
personalSubscription.recurlySubscription_id === ''
|
||||
) {
|
||||
return cb(null, null)
|
||||
}
|
||||
RecurlyWrapper.getSubscription(
|
||||
personalSubscription.recurlySubscription_id,
|
||||
{ includeAccount: true },
|
||||
cb
|
||||
)
|
||||
},
|
||||
},
|
||||
(err, results) => {
|
||||
if (err) {
|
||||
return callback(err)
|
||||
}
|
||||
let {
|
||||
personalSubscription,
|
||||
memberGroupSubscriptions,
|
||||
managedGroupSubscriptions,
|
||||
confirmedMemberAffiliations,
|
||||
managedInstitutions,
|
||||
managedPublishers,
|
||||
v1SubscriptionStatus,
|
||||
recurlySubscription,
|
||||
recurlyCoupons,
|
||||
plan,
|
||||
} = results
|
||||
if (memberGroupSubscriptions == null) {
|
||||
memberGroupSubscriptions = []
|
||||
}
|
||||
if (managedGroupSubscriptions == null) {
|
||||
managedGroupSubscriptions = []
|
||||
}
|
||||
if (confirmedMemberAffiliations == null) {
|
||||
confirmedMemberAffiliations = []
|
||||
}
|
||||
if (managedInstitutions == null) {
|
||||
managedInstitutions = []
|
||||
}
|
||||
if (v1SubscriptionStatus == null) {
|
||||
v1SubscriptionStatus = {}
|
||||
}
|
||||
if (recurlyCoupons == null) {
|
||||
recurlyCoupons = []
|
||||
}
|
||||
|
||||
if (
|
||||
personalSubscription &&
|
||||
typeof personalSubscription.toObject === 'function'
|
||||
) {
|
||||
// Downgrade from Mongoose object, so we can add a recurly and plan attribute
|
||||
personalSubscription = personalSubscription.toObject()
|
||||
}
|
||||
|
||||
if (plan != null) {
|
||||
personalSubscription.plan = plan
|
||||
}
|
||||
|
||||
if (personalSubscription && recurlySubscription) {
|
||||
const tax = recurlySubscription.tax_in_cents || 0
|
||||
// Some plans allow adding more seats than the base plan provides.
|
||||
// This is recorded as a subscription add on.
|
||||
// Note: tax_in_cents already includes the tax for any addon.
|
||||
let addOnPrice = 0
|
||||
let additionalLicenses = 0
|
||||
if (
|
||||
plan.membersLimitAddOn &&
|
||||
Array.isArray(recurlySubscription.subscription_add_ons)
|
||||
) {
|
||||
recurlySubscription.subscription_add_ons.forEach(addOn => {
|
||||
if (addOn.add_on_code === plan.membersLimitAddOn) {
|
||||
addOnPrice += addOn.quantity * addOn.unit_amount_in_cents
|
||||
additionalLicenses += addOn.quantity
|
||||
}
|
||||
})
|
||||
],
|
||||
recurlyCoupons: [
|
||||
'recurlySubscription',
|
||||
(cb, { recurlySubscription }) => {
|
||||
if (!recurlySubscription) {
|
||||
return cb(null, null)
|
||||
}
|
||||
const totalLicenses = (plan.membersLimit || 0) + additionalLicenses
|
||||
personalSubscription.recurly = {
|
||||
tax,
|
||||
taxRate: recurlySubscription.tax_rate
|
||||
? parseFloat(recurlySubscription.tax_rate._)
|
||||
: 0,
|
||||
billingDetailsLink: buildHostedLink(
|
||||
recurlySubscription,
|
||||
'billingDetails'
|
||||
),
|
||||
accountManagementLink: buildHostedLink(recurlySubscription),
|
||||
additionalLicenses,
|
||||
totalLicenses,
|
||||
nextPaymentDueAt: SubscriptionFormatters.formatDate(
|
||||
recurlySubscription.current_period_ends_at
|
||||
),
|
||||
currency: recurlySubscription.currency,
|
||||
state: recurlySubscription.state,
|
||||
trialEndsAtFormatted: SubscriptionFormatters.formatDate(
|
||||
recurlySubscription.trial_ends_at
|
||||
),
|
||||
trial_ends_at: recurlySubscription.trial_ends_at,
|
||||
activeCoupons: recurlyCoupons,
|
||||
account: recurlySubscription.account,
|
||||
const accountId = recurlySubscription.account.account_code
|
||||
RecurlyWrapper.getAccountActiveCoupons(accountId, cb)
|
||||
},
|
||||
],
|
||||
plan: [
|
||||
'personalSubscription',
|
||||
(cb, { personalSubscription }) => {
|
||||
if (personalSubscription == null) {
|
||||
return cb()
|
||||
}
|
||||
if (recurlySubscription.pending_subscription) {
|
||||
const pendingPlan = PlansLocator.findLocalPlanInSettings(
|
||||
recurlySubscription.pending_subscription.plan.plan_code
|
||||
const plan = PlansLocator.findLocalPlanInSettings(
|
||||
personalSubscription.planCode
|
||||
)
|
||||
if (plan == null) {
|
||||
return cb(
|
||||
new Error(
|
||||
`No plan found for planCode '${personalSubscription.planCode}'`
|
||||
)
|
||||
)
|
||||
if (pendingPlan == null) {
|
||||
return callback(
|
||||
new Error(
|
||||
`No plan found for planCode '${personalSubscription.planCode}'`
|
||||
)
|
||||
}
|
||||
cb(null, plan)
|
||||
},
|
||||
],
|
||||
memberGroupSubscriptions(cb) {
|
||||
SubscriptionLocator.getMemberSubscriptions(user, cb)
|
||||
},
|
||||
managedGroupSubscriptions(cb) {
|
||||
SubscriptionLocator.getManagedGroupSubscriptions(user, cb)
|
||||
},
|
||||
confirmedMemberAffiliations(cb) {
|
||||
InstitutionsGetter.getConfirmedAffiliations(user._id, cb)
|
||||
},
|
||||
managedInstitutions(cb) {
|
||||
InstitutionsGetter.getManagedInstitutions(user._id, cb)
|
||||
},
|
||||
managedPublishers(cb) {
|
||||
PublishersGetter.getManagedPublishers(user._id, cb)
|
||||
},
|
||||
v1SubscriptionStatus(cb) {
|
||||
V1SubscriptionManager.getSubscriptionStatusFromV1(
|
||||
user._id,
|
||||
(error, status, v1Id) => {
|
||||
if (error) {
|
||||
return cb(error)
|
||||
}
|
||||
cb(null, status)
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
(err, results) => {
|
||||
if (err) {
|
||||
return callback(err)
|
||||
}
|
||||
let {
|
||||
personalSubscription,
|
||||
memberGroupSubscriptions,
|
||||
managedGroupSubscriptions,
|
||||
confirmedMemberAffiliations,
|
||||
managedInstitutions,
|
||||
managedPublishers,
|
||||
v1SubscriptionStatus,
|
||||
recurlySubscription,
|
||||
recurlyCoupons,
|
||||
plan,
|
||||
} = results
|
||||
if (memberGroupSubscriptions == null) {
|
||||
memberGroupSubscriptions = []
|
||||
}
|
||||
if (managedGroupSubscriptions == null) {
|
||||
managedGroupSubscriptions = []
|
||||
}
|
||||
if (confirmedMemberAffiliations == null) {
|
||||
confirmedMemberAffiliations = []
|
||||
}
|
||||
if (managedInstitutions == null) {
|
||||
managedInstitutions = []
|
||||
}
|
||||
if (v1SubscriptionStatus == null) {
|
||||
v1SubscriptionStatus = {}
|
||||
}
|
||||
if (recurlyCoupons == null) {
|
||||
recurlyCoupons = []
|
||||
}
|
||||
|
||||
if (
|
||||
personalSubscription &&
|
||||
typeof personalSubscription.toObject === 'function'
|
||||
) {
|
||||
// Downgrade from Mongoose object, so we can add a recurly and plan attribute
|
||||
personalSubscription = personalSubscription.toObject()
|
||||
}
|
||||
|
||||
if (plan != null) {
|
||||
personalSubscription.plan = plan
|
||||
}
|
||||
|
||||
if (personalSubscription && recurlySubscription) {
|
||||
const tax = recurlySubscription.tax_in_cents || 0
|
||||
// Some plans allow adding more seats than the base plan provides.
|
||||
// This is recorded as a subscription add on.
|
||||
// Note: tax_in_cents already includes the tax for any addon.
|
||||
let addOnPrice = 0
|
||||
let additionalLicenses = 0
|
||||
if (
|
||||
plan.membersLimitAddOn &&
|
||||
Array.isArray(recurlySubscription.subscription_add_ons)
|
||||
) {
|
||||
recurlySubscription.subscription_add_ons.forEach(addOn => {
|
||||
if (addOn.add_on_code === plan.membersLimitAddOn) {
|
||||
addOnPrice += addOn.quantity * addOn.unit_amount_in_cents
|
||||
additionalLicenses += addOn.quantity
|
||||
}
|
||||
})
|
||||
}
|
||||
const totalLicenses = (plan.membersLimit || 0) + additionalLicenses
|
||||
personalSubscription.recurly = {
|
||||
tax,
|
||||
taxRate: recurlySubscription.tax_rate
|
||||
? parseFloat(recurlySubscription.tax_rate._)
|
||||
: 0,
|
||||
billingDetailsLink: buildHostedLink(
|
||||
recurlySubscription,
|
||||
'billingDetails'
|
||||
),
|
||||
accountManagementLink: buildHostedLink(recurlySubscription),
|
||||
additionalLicenses,
|
||||
totalLicenses,
|
||||
nextPaymentDueAt: SubscriptionFormatters.formatDate(
|
||||
recurlySubscription.current_period_ends_at
|
||||
),
|
||||
currency: recurlySubscription.currency,
|
||||
state: recurlySubscription.state,
|
||||
trialEndsAtFormatted: SubscriptionFormatters.formatDate(
|
||||
recurlySubscription.trial_ends_at
|
||||
),
|
||||
trial_ends_at: recurlySubscription.trial_ends_at,
|
||||
activeCoupons: recurlyCoupons,
|
||||
account: recurlySubscription.account,
|
||||
}
|
||||
if (recurlySubscription.pending_subscription) {
|
||||
const pendingPlan = PlansLocator.findLocalPlanInSettings(
|
||||
recurlySubscription.pending_subscription.plan.plan_code
|
||||
)
|
||||
if (pendingPlan == null) {
|
||||
return callback(
|
||||
new Error(
|
||||
`No plan found for planCode '${personalSubscription.planCode}'`
|
||||
)
|
||||
)
|
||||
}
|
||||
let pendingAdditionalLicenses = 0
|
||||
let pendingAddOnTax = 0
|
||||
let pendingAddOnPrice = 0
|
||||
if (recurlySubscription.pending_subscription.subscription_add_ons) {
|
||||
if (
|
||||
pendingPlan.membersLimitAddOn &&
|
||||
Array.isArray(
|
||||
recurlySubscription.pending_subscription.subscription_add_ons
|
||||
)
|
||||
) {
|
||||
recurlySubscription.pending_subscription.subscription_add_ons.forEach(
|
||||
addOn => {
|
||||
if (addOn.add_on_code === pendingPlan.membersLimitAddOn) {
|
||||
pendingAddOnPrice +=
|
||||
addOn.quantity * addOn.unit_amount_in_cents
|
||||
pendingAdditionalLicenses += addOn.quantity
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
let pendingAdditionalLicenses = 0
|
||||
let pendingAddOnTax = 0
|
||||
let pendingAddOnPrice = 0
|
||||
if (recurlySubscription.pending_subscription.subscription_add_ons) {
|
||||
if (
|
||||
pendingPlan.membersLimitAddOn &&
|
||||
Array.isArray(
|
||||
recurlySubscription.pending_subscription.subscription_add_ons
|
||||
)
|
||||
) {
|
||||
recurlySubscription.pending_subscription.subscription_add_ons.forEach(
|
||||
addOn => {
|
||||
if (addOn.add_on_code === pendingPlan.membersLimitAddOn) {
|
||||
pendingAddOnPrice +=
|
||||
addOn.quantity * addOn.unit_amount_in_cents
|
||||
pendingAdditionalLicenses += addOn.quantity
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
// Need to calculate tax ourselves as we don't get tax amounts for pending subs
|
||||
pendingAddOnTax =
|
||||
personalSubscription.recurly.taxRate * pendingAddOnPrice
|
||||
}
|
||||
const pendingSubscriptionTax =
|
||||
personalSubscription.recurly.taxRate *
|
||||
recurlySubscription.pending_subscription.unit_amount_in_cents
|
||||
personalSubscription.recurly.price = SubscriptionFormatters.formatPrice(
|
||||
recurlySubscription.pending_subscription.unit_amount_in_cents +
|
||||
pendingAddOnPrice +
|
||||
pendingAddOnTax +
|
||||
pendingSubscriptionTax,
|
||||
recurlySubscription.currency
|
||||
)
|
||||
const pendingTotalLicenses =
|
||||
(pendingPlan.membersLimit || 0) + pendingAdditionalLicenses
|
||||
personalSubscription.recurly.pendingAdditionalLicenses = pendingAdditionalLicenses
|
||||
personalSubscription.recurly.pendingTotalLicenses = pendingTotalLicenses
|
||||
personalSubscription.pendingPlan = pendingPlan
|
||||
} else {
|
||||
personalSubscription.recurly.price = SubscriptionFormatters.formatPrice(
|
||||
recurlySubscription.unit_amount_in_cents + addOnPrice + tax,
|
||||
recurlySubscription.currency
|
||||
)
|
||||
// Need to calculate tax ourselves as we don't get tax amounts for pending subs
|
||||
pendingAddOnTax =
|
||||
personalSubscription.recurly.taxRate * pendingAddOnPrice
|
||||
}
|
||||
const pendingSubscriptionTax =
|
||||
personalSubscription.recurly.taxRate *
|
||||
recurlySubscription.pending_subscription.unit_amount_in_cents
|
||||
personalSubscription.recurly.price = SubscriptionFormatters.formatPrice(
|
||||
recurlySubscription.pending_subscription.unit_amount_in_cents +
|
||||
pendingAddOnPrice +
|
||||
pendingAddOnTax +
|
||||
pendingSubscriptionTax,
|
||||
recurlySubscription.currency
|
||||
)
|
||||
const pendingTotalLicenses =
|
||||
(pendingPlan.membersLimit || 0) + pendingAdditionalLicenses
|
||||
personalSubscription.recurly.pendingAdditionalLicenses = pendingAdditionalLicenses
|
||||
personalSubscription.recurly.pendingTotalLicenses = pendingTotalLicenses
|
||||
personalSubscription.pendingPlan = pendingPlan
|
||||
} else {
|
||||
personalSubscription.recurly.price = SubscriptionFormatters.formatPrice(
|
||||
recurlySubscription.unit_amount_in_cents + addOnPrice + tax,
|
||||
recurlySubscription.currency
|
||||
)
|
||||
}
|
||||
|
||||
for (const memberGroupSubscription of memberGroupSubscriptions) {
|
||||
if (memberGroupSubscription.teamNotice) {
|
||||
memberGroupSubscription.teamNotice = sanitizeHtml(
|
||||
memberGroupSubscription.teamNotice
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
callback(null, {
|
||||
personalSubscription,
|
||||
managedGroupSubscriptions,
|
||||
memberGroupSubscriptions,
|
||||
confirmedMemberAffiliations,
|
||||
managedInstitutions,
|
||||
managedPublishers,
|
||||
v1SubscriptionStatus,
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
buildPlansList(currentPlan) {
|
||||
const { plans } = Settings
|
||||
for (const memberGroupSubscription of memberGroupSubscriptions) {
|
||||
if (memberGroupSubscription.teamNotice) {
|
||||
memberGroupSubscription.teamNotice = sanitizeHtml(
|
||||
memberGroupSubscription.teamNotice
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const allPlans = {}
|
||||
plans.forEach(plan => {
|
||||
allPlans[plan.planCode] = plan
|
||||
})
|
||||
|
||||
const result = { allPlans }
|
||||
|
||||
if (currentPlan) {
|
||||
result.planCodesChangingAtTermEnd = _.pluck(
|
||||
_.filter(plans, plan => {
|
||||
if (!plan.hideFromUsers) {
|
||||
return SubscriptionHelper.shouldPlanChangeAtTermEnd(
|
||||
currentPlan,
|
||||
plan
|
||||
)
|
||||
}
|
||||
}),
|
||||
'planCode'
|
||||
)
|
||||
callback(null, {
|
||||
personalSubscription,
|
||||
managedGroupSubscriptions,
|
||||
memberGroupSubscriptions,
|
||||
confirmedMemberAffiliations,
|
||||
managedInstitutions,
|
||||
managedPublishers,
|
||||
v1SubscriptionStatus,
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
result.studentAccounts = _.filter(
|
||||
plans,
|
||||
plan => plan.planCode.indexOf('student') !== -1
|
||||
function buildPlansList(currentPlan) {
|
||||
const { plans } = Settings
|
||||
|
||||
const allPlans = {}
|
||||
plans.forEach(plan => {
|
||||
allPlans[plan.planCode] = plan
|
||||
})
|
||||
|
||||
const result = { allPlans }
|
||||
|
||||
if (currentPlan) {
|
||||
result.planCodesChangingAtTermEnd = _.pluck(
|
||||
_.filter(plans, plan => {
|
||||
if (!plan.hideFromUsers) {
|
||||
return SubscriptionHelper.shouldPlanChangeAtTermEnd(currentPlan, plan)
|
||||
}
|
||||
}),
|
||||
'planCode'
|
||||
)
|
||||
}
|
||||
|
||||
result.groupMonthlyPlans = _.filter(
|
||||
plans,
|
||||
plan => plan.groupPlan && !plan.annual
|
||||
)
|
||||
result.studentAccounts = _.filter(
|
||||
plans,
|
||||
plan => plan.planCode.indexOf('student') !== -1
|
||||
)
|
||||
|
||||
result.groupAnnualPlans = _.filter(
|
||||
plans,
|
||||
plan => plan.groupPlan && plan.annual
|
||||
)
|
||||
result.groupMonthlyPlans = _.filter(
|
||||
plans,
|
||||
plan => plan.groupPlan && !plan.annual
|
||||
)
|
||||
|
||||
result.individualMonthlyPlans = _.filter(
|
||||
plans,
|
||||
plan =>
|
||||
!plan.groupPlan &&
|
||||
!plan.annual &&
|
||||
plan.planCode !== 'personal' && // Prevent the personal plan from appearing on the change-plans page
|
||||
plan.planCode.indexOf('student') === -1
|
||||
)
|
||||
result.groupAnnualPlans = _.filter(
|
||||
plans,
|
||||
plan => plan.groupPlan && plan.annual
|
||||
)
|
||||
|
||||
result.individualAnnualPlans = _.filter(
|
||||
plans,
|
||||
plan =>
|
||||
!plan.groupPlan &&
|
||||
plan.annual &&
|
||||
plan.planCode.indexOf('student') === -1
|
||||
)
|
||||
result.individualMonthlyPlans = _.filter(
|
||||
plans,
|
||||
plan =>
|
||||
!plan.groupPlan &&
|
||||
!plan.annual &&
|
||||
plan.planCode !== 'personal' && // Prevent the personal plan from appearing on the change-plans page
|
||||
plan.planCode.indexOf('student') === -1
|
||||
)
|
||||
|
||||
return result
|
||||
result.individualAnnualPlans = _.filter(
|
||||
plans,
|
||||
plan =>
|
||||
!plan.groupPlan && plan.annual && plan.planCode.indexOf('student') === -1
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildUsersSubscriptionViewModel,
|
||||
buildPlansList,
|
||||
promises: {
|
||||
buildUsersSubscriptionViewModel: promisify(buildUsersSubscriptionViewModel),
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user