fix: ts types
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
MOCHA_PARALLEL=true npx mocha 'tests/alex/00_setup.ts' && npx mocha --parallel --timeout 10000000 \
|
||||
# npx mocha 'tests/alex/00_setup.ts' --timeout 10000000
|
||||
|
||||
MOCHA_PARALLEL=true npx mocha --parallel --timeout 10000000 \
|
||||
'tests/alex/01_free.ts' 'tests/alex/02_pro.ts' 'tests/alex/03_premium.ts' \
|
||||
'tests/alex/04_topups.ts' 'tests/alex/05_cancel.ts' 'tests/alex/06_switch.ts' \
|
||||
--ignore 'tests/alex/00_setup.ts'
|
||||
@@ -62,7 +62,7 @@ export const findPriceInStripeItems = ({
|
||||
}: {
|
||||
prices: Price[];
|
||||
subItem?: Stripe.SubscriptionItem;
|
||||
lineItem?: Stripe.InvoiceItem;
|
||||
lineItem?: Stripe.InvoiceItem | Stripe.InvoiceLineItem;
|
||||
billingType?: BillingType;
|
||||
}) => {
|
||||
return prices.find((p: Price) => {
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
import Stripe from "stripe";
|
||||
import {
|
||||
BillingInterval,
|
||||
AttachConfig,
|
||||
ProrationBehavior,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import { SubService } from "@/internal/subscriptions/SubService.js";
|
||||
import { ItemSet } from "@/utils/models/ItemSet.js";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||
import { createProrationInvoice } from "./updateStripeSub/createProrationinvoice.js";
|
||||
import {
|
||||
createUsageInvoiceItems,
|
||||
resetUsageBalances,
|
||||
} from "@/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoiceItems.js";
|
||||
import { attachParamToCusProducts } from "@/internal/customers/attach/attachUtils/convertAttachParams.js";
|
||||
import { createAndFilterContUseItems } from "@/internal/customers/attach/attachUtils/getContUseItems/createContUseInvoiceItems.js";
|
||||
|
||||
export const updateStripeSubscription = async ({
|
||||
db,
|
||||
attachParams,
|
||||
config,
|
||||
trialEnd,
|
||||
stripeSubs,
|
||||
itemSet,
|
||||
logger,
|
||||
interval,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
attachParams: AttachParams;
|
||||
config: AttachConfig;
|
||||
stripeSubs: Stripe.Subscription[];
|
||||
trialEnd?: number;
|
||||
itemSet: ItemSet;
|
||||
shouldPreview?: boolean;
|
||||
logger: any;
|
||||
interval?: BillingInterval;
|
||||
}) => {
|
||||
const { curMainProduct } = attachParamToCusProducts({ attachParams });
|
||||
const { stripeCli, customer, org, paymentMethod } = attachParams;
|
||||
const { invoiceOnly, proration } = config;
|
||||
const curSub = stripeSubs[0];
|
||||
|
||||
let { items, prices } = itemSet;
|
||||
|
||||
let subItems = items.filter(
|
||||
(i: any, index: number) =>
|
||||
i.deleted || prices[index].config!.interval !== BillingInterval.OneOff
|
||||
);
|
||||
|
||||
let subInvoiceItems = items.filter((i: any, index: number) => {
|
||||
if (index < prices.length) {
|
||||
return prices[index].config!.interval === BillingInterval.OneOff;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// 1. Update subscription
|
||||
let sub: Stripe.Subscription | null = null;
|
||||
let stripeProration =
|
||||
proration == ProrationBehavior.None ? "none" : "create_prorations";
|
||||
|
||||
try {
|
||||
sub = await stripeCli.subscriptions.update(curSub.id, {
|
||||
items: subItems,
|
||||
proration_behavior: stripeProration,
|
||||
trial_end: trialEnd,
|
||||
default_payment_method: paymentMethod?.id,
|
||||
add_invoice_items: subInvoiceItems,
|
||||
...((invoiceOnly && {
|
||||
collection_method: "send_invoice",
|
||||
days_until_due: 30,
|
||||
}) as any),
|
||||
payment_behavior: "error_if_incomplete",
|
||||
expand: ["latest_invoice"],
|
||||
});
|
||||
} catch (error: any) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const latestInvoice = sub.latest_invoice as Stripe.Invoice;
|
||||
|
||||
if (proration == ProrationBehavior.None) {
|
||||
return {
|
||||
preview: null,
|
||||
sub: {
|
||||
...sub,
|
||||
latest_invoice: latestInvoice.id,
|
||||
},
|
||||
invoice: null,
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Create invoice items for remaining usages
|
||||
let invoice: Stripe.Invoice | null = null;
|
||||
let { cusEntIds } = await createUsageInvoiceItems({
|
||||
db,
|
||||
attachParams,
|
||||
cusProduct: curMainProduct!,
|
||||
stripeSubs,
|
||||
logger,
|
||||
});
|
||||
|
||||
await createAndFilterContUseItems({
|
||||
attachParams,
|
||||
curMainProduct: curMainProduct!,
|
||||
stripeSubs,
|
||||
interval,
|
||||
logger,
|
||||
});
|
||||
|
||||
if (proration === ProrationBehavior.Immediately) {
|
||||
if (latestInvoice.id != curSub.latest_invoice) {
|
||||
sub.latest_invoice = latestInvoice.id;
|
||||
return {
|
||||
preview: null,
|
||||
sub,
|
||||
invoice: latestInvoice,
|
||||
};
|
||||
}
|
||||
|
||||
invoice = await createProrationInvoice({
|
||||
attachParams,
|
||||
invoiceOnly,
|
||||
curSub,
|
||||
updatedSub: sub,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
|
||||
await resetUsageBalances({
|
||||
db,
|
||||
cusEntIds,
|
||||
cusProduct: curMainProduct!,
|
||||
});
|
||||
|
||||
// Upsert sub
|
||||
await SubService.addUsageFeatures({
|
||||
db,
|
||||
stripeId: curSub.id,
|
||||
usageFeatures: itemSet.usageFeatures,
|
||||
orgId: org.id,
|
||||
env: customer.env,
|
||||
});
|
||||
|
||||
if (invoice) {
|
||||
sub.latest_invoice = invoice.id;
|
||||
} else {
|
||||
sub.latest_invoice = null;
|
||||
}
|
||||
|
||||
return {
|
||||
preview: null,
|
||||
sub,
|
||||
invoice,
|
||||
};
|
||||
};
|
||||
|
||||
// if (shouldPreview) {
|
||||
// let preview = await stripeCli.invoices.createPreview({
|
||||
// subscription_details: {
|
||||
// items: subItems,
|
||||
// proration_behavior: getStripeProrationBehavior({
|
||||
// org,
|
||||
// prorationBehavior: proration,
|
||||
// }) as any,
|
||||
// trial_end: trialEnd as any,
|
||||
// },
|
||||
// subscription: curSub.id,
|
||||
// invoice_items: subInvoiceItems,
|
||||
// customer: customer.processor.id,
|
||||
// });
|
||||
// return {
|
||||
// preview,
|
||||
// sub: null,
|
||||
// invoice: null,
|
||||
// };
|
||||
// }
|
||||
@@ -1,37 +0,0 @@
|
||||
import Stripe from "stripe";
|
||||
import { BillingType, Feature, FeatureUsageType } from "@autumn/shared";
|
||||
import { cusProductToPrices } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
|
||||
import { FullCusProduct } from "@autumn/shared";
|
||||
import { findPriceInStripeItems } from "../stripeSubItemUtils.js";
|
||||
import { priceToFeature } from "@/internal/products/prices/priceUtils/convertPrice.js";
|
||||
|
||||
// For prepaid single use prices...
|
||||
export const removePrepaidSingleUseItems = async ({
|
||||
stripeCli,
|
||||
curCusProduct,
|
||||
features,
|
||||
invoice,
|
||||
}: {
|
||||
stripeCli: Stripe;
|
||||
curCusProduct: FullCusProduct;
|
||||
features: Feature[];
|
||||
invoice: Stripe.Invoice;
|
||||
}) => {
|
||||
const curPrices = cusProductToPrices({ cusProduct: curCusProduct });
|
||||
|
||||
for (const item of invoice.lines.data) {
|
||||
let price = findPriceInStripeItems({
|
||||
prices: curPrices,
|
||||
subItem: item,
|
||||
billingType: BillingType.UsageInAdvance,
|
||||
});
|
||||
|
||||
if (!price) continue;
|
||||
|
||||
let feature = priceToFeature({ price, features })!;
|
||||
if (feature.config.usage_type == FeatureUsageType.Single) {
|
||||
console.log("Deleting item:", item.id, "Feature:", feature.id);
|
||||
await stripeCli.invoiceItems.del(item.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -95,7 +95,7 @@ export const createAndFilterContUseItems = async ({
|
||||
// return { newItems: [], oldItems: [], replaceables: [] };
|
||||
// }
|
||||
|
||||
let { newItems, oldItems } = await getContUseInvoiceItems({
|
||||
let { newItems, oldItems, replaceables } = await getContUseInvoiceItems({
|
||||
attachParams,
|
||||
cusProduct: curMainProduct!,
|
||||
sub,
|
||||
@@ -154,5 +154,5 @@ export const createAndFilterContUseItems = async ({
|
||||
});
|
||||
}
|
||||
|
||||
return { newItems, oldItems };
|
||||
return { newItems, oldItems, replaceables };
|
||||
};
|
||||
|
||||
@@ -32,6 +32,7 @@ export const constructFeature = ({
|
||||
type,
|
||||
config,
|
||||
display,
|
||||
archived: false,
|
||||
};
|
||||
|
||||
return newFeature;
|
||||
@@ -58,6 +59,7 @@ export const constructBooleanFeature = ({
|
||||
name: name || keyToTitle(featureId),
|
||||
type: FeatureType.Boolean,
|
||||
config: null,
|
||||
archived: false,
|
||||
};
|
||||
|
||||
return newFeature;
|
||||
@@ -99,6 +101,7 @@ export const constructMeteredFeature = ({
|
||||
},
|
||||
usage_type: usageType,
|
||||
},
|
||||
archived: false,
|
||||
};
|
||||
|
||||
return newFeature;
|
||||
@@ -139,6 +142,7 @@ export const constructCreditSystem = ({
|
||||
name: name || keyToTitle(featureId),
|
||||
type: FeatureType.CreditSystem,
|
||||
config,
|
||||
archived: false,
|
||||
};
|
||||
|
||||
return newFeature;
|
||||
|
||||
@@ -39,7 +39,7 @@ export const processInvoice = ({
|
||||
items: withItems
|
||||
? (invoice.items || []).map((i) => {
|
||||
let feature = features?.find(
|
||||
(f) => f.internal_id === i.internal_feature_id,
|
||||
(f) => f.internal_id === i.internal_feature_id
|
||||
);
|
||||
|
||||
return InvoiceItemResponseSchema.parse({
|
||||
@@ -84,7 +84,7 @@ export class InvoiceService {
|
||||
eq(invoices.internal_customer_id, internalCustomerId),
|
||||
internalEntityId
|
||||
? eq(invoices.internal_entity_id, internalEntityId)
|
||||
: undefined,
|
||||
: undefined
|
||||
),
|
||||
orderBy: [desc(invoices.created_at)],
|
||||
limit,
|
||||
@@ -146,7 +146,7 @@ export class InvoiceService {
|
||||
internal_customer_id: internalCustomerId,
|
||||
product_ids: uniqueProductIds,
|
||||
created_at: stripeInvoice.created * 1000,
|
||||
stripe_id: stripeInvoice.id,
|
||||
stripe_id: stripeInvoice.id!,
|
||||
hosted_invoice_url: stripeInvoice.hosted_invoice_url || null,
|
||||
status: status || (stripeInvoice.status as InvoiceStatus | null),
|
||||
internal_product_ids: uniqueInternalProductIds,
|
||||
|
||||
@@ -31,7 +31,7 @@ export const insertInvoiceFromAttach = async ({
|
||||
// Create or update
|
||||
let invoice = await InvoiceService.getByStripeId({
|
||||
db,
|
||||
stripeId: stripeInvoice.id,
|
||||
stripeId: stripeInvoice.id!,
|
||||
});
|
||||
|
||||
let autumnInvoiceItems = await getInvoiceItems({
|
||||
@@ -43,7 +43,7 @@ export const insertInvoiceFromAttach = async ({
|
||||
if (invoice) {
|
||||
await InvoiceService.updateByStripeId({
|
||||
db,
|
||||
stripeId: stripeInvoice.id,
|
||||
stripeId: stripeInvoice.id!,
|
||||
updates: {
|
||||
product_ids: attachParams.products.map((p) => p.id),
|
||||
internal_product_ids: attachParams.products.map((p) => p.internal_id),
|
||||
@@ -99,7 +99,7 @@ export const getInvoiceItems = async ({
|
||||
for (const line of stripeInvoice.lines.data) {
|
||||
let price = findPriceInStripeItems({
|
||||
prices,
|
||||
subItem: line,
|
||||
lineItem: line,
|
||||
});
|
||||
|
||||
if (!price) {
|
||||
|
||||
@@ -30,13 +30,13 @@ export const createAndFinalizeInvoice = async ({
|
||||
for (const item of invoiceItems) {
|
||||
await stripeCli.invoiceItems.create({
|
||||
...item,
|
||||
invoice: invoice.id,
|
||||
invoice: invoice.id!,
|
||||
customer: stripeCusId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let finalInvoice = await stripeCli.invoices.finalizeInvoice(invoice.id, {
|
||||
let finalInvoice = await stripeCli.invoices.finalizeInvoice(invoice.id!, {
|
||||
auto_advance: false,
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ export const createAndFinalizeInvoice = async ({
|
||||
invoice: paidInvoice,
|
||||
} = await payForInvoice({
|
||||
stripeCli,
|
||||
invoiceId: finalInvoice.id,
|
||||
invoiceId: finalInvoice.id!,
|
||||
paymentMethod,
|
||||
logger,
|
||||
errorOnFail: errorOnPaymentFail,
|
||||
|
||||
@@ -303,7 +303,7 @@ export class ProductService {
|
||||
parseFreeTrials({ product: data });
|
||||
|
||||
if (!data) {
|
||||
if (allowNotFound) return null;
|
||||
if (allowNotFound) return null as unknown as FullProduct;
|
||||
throw new RecaseError({
|
||||
message: `Product ${idOrInternalId} not found`,
|
||||
code: ErrCode.ProductNotFound,
|
||||
|
||||
@@ -20,12 +20,13 @@ import { isUsagePrice } from "@/internal/products/prices/priceUtils/usagePriceUt
|
||||
import { subToAutumnInterval } from "@/external/stripe/utils.js";
|
||||
import { prices as priceTable } from "@autumn/shared";
|
||||
import { PriceService } from "@/internal/products/prices/PriceService.js";
|
||||
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
|
||||
|
||||
export const addProductFromSubs = async ({
|
||||
req,
|
||||
autumnCus,
|
||||
autumnProduct,
|
||||
stripeSubs,
|
||||
sub,
|
||||
prices,
|
||||
entitlements,
|
||||
force = false,
|
||||
@@ -35,7 +36,7 @@ export const addProductFromSubs = async ({
|
||||
req: ExtendedRequest;
|
||||
autumnCus: FullCustomer;
|
||||
autumnProduct: FullProduct;
|
||||
stripeSubs: Stripe.Subscription[];
|
||||
sub: Stripe.Subscription;
|
||||
prices?: Price[];
|
||||
entitlements?: EntitlementWithFeature[];
|
||||
force?: boolean;
|
||||
@@ -71,9 +72,7 @@ export const addProductFromSubs = async ({
|
||||
}
|
||||
|
||||
// Handle if trialing
|
||||
let trialEndsAt = stripeSubs?.[0]?.trial_end
|
||||
? stripeSubs[0].trial_end * 1000
|
||||
: null;
|
||||
let trialEndsAt = sub?.trial_end ? sub.trial_end * 1000 : null;
|
||||
|
||||
// throw new Error("test");
|
||||
|
||||
@@ -86,6 +85,10 @@ export const addProductFromSubs = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const { start, end } = subToPeriodStartEnd({
|
||||
sub,
|
||||
});
|
||||
|
||||
let newCusProduct = await createFullCusProduct({
|
||||
db,
|
||||
attachParams: {
|
||||
@@ -108,18 +111,16 @@ export const addProductFromSubs = async ({
|
||||
},
|
||||
logger,
|
||||
trialEndsAt: trialEndsAt || undefined,
|
||||
subscriptionIds: stripeSubs.map((s) => s.id),
|
||||
anchorToUnix: anchorToUnix || stripeSubs?.[0]?.current_period_end * 1000,
|
||||
subscriptionIds: sub ? [sub.id] : [],
|
||||
anchorToUnix: anchorToUnix || end,
|
||||
|
||||
subscriptionStatus: stripeSubs?.[0]?.status
|
||||
? (stripeToAutumnSubStatus(stripeSubs[0].status) as CusProductStatus)
|
||||
subscriptionStatus: sub?.status
|
||||
? (stripeToAutumnSubStatus(sub?.status) as CusProductStatus)
|
||||
: undefined,
|
||||
|
||||
canceledAt: stripeSubs?.[0]?.canceled_at
|
||||
? stripeSubs[0].canceled_at * 1000
|
||||
: null,
|
||||
canceledAt: sub?.canceled_at ? sub.canceled_at * 1000 : null,
|
||||
|
||||
createdAt: stripeSubs?.[0]?.created ? stripeSubs[0].created * 1000 : null,
|
||||
createdAt: sub?.created ? sub.created * 1000 : null,
|
||||
sendWebhook: false,
|
||||
});
|
||||
|
||||
@@ -132,31 +133,29 @@ export const addProductFromSubs = async ({
|
||||
.filter((p) => isUsagePrice({ price: p }))
|
||||
.map((p) => (p.config as UsagePriceConfig).internal_feature_id);
|
||||
|
||||
for (const sub of stripeSubs) {
|
||||
let subFromDb = await SubService.getInStripeIds({
|
||||
let subFromDb = await SubService.getInStripeIds({
|
||||
db,
|
||||
ids: [sub.id],
|
||||
});
|
||||
|
||||
let subInterval = subToAutumnInterval(sub);
|
||||
|
||||
if (subFromDb.length === 0) {
|
||||
await SubService.createSub({
|
||||
db,
|
||||
ids: [sub.id],
|
||||
sub: constructSub({
|
||||
stripeId: sub.id,
|
||||
usageFeatures:
|
||||
subInterval.interval == BillingInterval.Month ? usageFeatures : [],
|
||||
orgId: org.id,
|
||||
env,
|
||||
currentPeriodStart: start,
|
||||
currentPeriodEnd: end,
|
||||
}),
|
||||
});
|
||||
|
||||
let subInterval = subToAutumnInterval(sub);
|
||||
|
||||
if (subFromDb.length === 0) {
|
||||
await SubService.createSub({
|
||||
db,
|
||||
sub: constructSub({
|
||||
stripeId: sub.id,
|
||||
usageFeatures:
|
||||
subInterval.interval == BillingInterval.Month ? usageFeatures : [],
|
||||
orgId: org.id,
|
||||
env,
|
||||
currentPeriodStart: sub.current_period_start,
|
||||
currentPeriodEnd: sub.current_period_end,
|
||||
}),
|
||||
});
|
||||
logger.info(`Created sub ${sub.id} in DB`);
|
||||
} else {
|
||||
logger.info(`Sub ${sub.id} already exists in DB`);
|
||||
}
|
||||
logger.info(`Created sub ${sub.id} in DB`);
|
||||
} else {
|
||||
logger.info(`Sub ${sub.id} already exists in DB`);
|
||||
}
|
||||
|
||||
autumnCus.customer_products = [
|
||||
|
||||
@@ -8,8 +8,6 @@ const DEFAULT_ENV = AppEnv.Sandbox;
|
||||
|
||||
describe("Initialize org for tests", () => {
|
||||
it("should initialize org", async function () {
|
||||
this.timeout(20000);
|
||||
|
||||
this.org = await clearOrg({ orgSlug: ORG_SLUG, env: DEFAULT_ENV });
|
||||
this.env = DEFAULT_ENV;
|
||||
await setupOrg({
|
||||
|
||||
@@ -65,14 +65,14 @@ describe(chalk.yellowBright("Top ups"), () => {
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
// Get product
|
||||
const product = cusRes.add_ons.find(
|
||||
(p: any) => p.id === alexProducts.topUpMessages.id,
|
||||
(p: any) => p.id === alexProducts.topUpMessages.id
|
||||
);
|
||||
expect(product).to.exist;
|
||||
expect(product.status).to.equal(CusProductStatus.Active);
|
||||
|
||||
// Check quantity is correct
|
||||
let cusEnt = cusRes.entitlements.find(
|
||||
(e: any) => e.feature_id === alexFeatures.topUpMessage.id,
|
||||
(e: any) => e.feature_id === alexFeatures.topUpMessage.id
|
||||
);
|
||||
|
||||
expect(cusEnt).to.exist;
|
||||
@@ -86,7 +86,7 @@ describe(chalk.yellowBright("Top ups"), () => {
|
||||
const { allowed, balanceObj }: any = await AutumnCli.entitled(
|
||||
customerId,
|
||||
alexFeatures.topUpMessage.id,
|
||||
true,
|
||||
true
|
||||
);
|
||||
leftoverBalance = balanceObj.balance;
|
||||
|
||||
@@ -102,18 +102,18 @@ describe(chalk.yellowBright("Top ups"), () => {
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
// Get product
|
||||
const product = cusRes.add_ons.find(
|
||||
(p: any) => p.id === alexProducts.topUpMessages.id,
|
||||
(p: any) => p.id === alexProducts.topUpMessages.id
|
||||
);
|
||||
expect(product).to.exist;
|
||||
expect(product.status).to.equal(CusProductStatus.Active);
|
||||
|
||||
// Check quantity is correct
|
||||
let cusEnt = cusRes.entitlements.find(
|
||||
(e: any) => e.feature_id === alexFeatures.topUpMessage.id,
|
||||
(e: any) => e.feature_id === alexFeatures.topUpMessage.id
|
||||
);
|
||||
expect(cusEnt).to.exist;
|
||||
expect(cusEnt.balance).to.equal(
|
||||
leftoverBalance + overrideQuantity * billingUnits,
|
||||
leftoverBalance + overrideQuantity * billingUnits
|
||||
);
|
||||
expect(cusEnt.interval).to.equal(prodEnt.interval);
|
||||
});
|
||||
@@ -166,7 +166,7 @@ describe(chalk.yellowBright("Testing o1 message top up"), () => {
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
// Get product
|
||||
const product = cusRes.add_ons.find(
|
||||
(p: any) => p.id === alexProducts.o1TopUps.id,
|
||||
(p: any) => p.id === alexProducts.o1TopUps.id
|
||||
);
|
||||
|
||||
expect(product).to.exist;
|
||||
|
||||
@@ -184,7 +184,7 @@ export const runEventsAndCheckBalances = async ({
|
||||
);
|
||||
}
|
||||
|
||||
let timeoutMilli = Math.max(Math.floor(firstHalf / 2.5), 2) * 1000;
|
||||
let timeoutMilli = Math.max(Math.floor(firstHalf / 2.5), 2) * 2500;
|
||||
await timeout(timeoutMilli);
|
||||
|
||||
await Promise.all(batchUpdate);
|
||||
|
||||
Reference in New Issue
Block a user