Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow purchasing additional seats in workspace #169

Merged
merged 3 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app-server/src/db/stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ pub struct WorkspaceStats {

pub members: i64,
pub members_limit: i64,
pub seats_included_in_tier: i64,
pub reset_time: DateTime<Utc>,
pub storage_limit: i64,
}
Expand All @@ -103,6 +104,7 @@ pub async fn get_workspace_stats(pool: &PgPool, workspace_id: &Uuid) -> Result<W
)
SELECT
subscription_tiers.name as tier_name,
subscription_tiers.members_per_workspace as seats_included_in_tier,
workspace_usage.span_count as total_spans,
workspace_usage.event_count as total_events,
subscription_tiers.spans as spans_limit,
Expand Down
21 changes: 14 additions & 7 deletions app-server/src/db/subscriptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,10 @@ struct SubscriptionTierId {
id: i64,
}

pub async fn add_seats(pool: &PgPool, workspace_id: &Uuid, seats: i64) -> Result<()> {
pub async fn set_seats(pool: &PgPool, workspace_id: &Uuid, seats: i64) -> Result<()> {
sqlx::query(
"UPDATE workspaces SET
additional_seats = additional_seats + $2
additional_seats = $2
WHERE workspaces.id = $1",
)
.bind(workspace_id)
Expand All @@ -103,6 +103,7 @@ pub async fn update_subscription(
pool: &PgPool,
workspace_id: &Uuid,
product_id: &String,
subscription_id: &String,
cancel: bool,
) -> Result<bool> {
let product_id = if cancel { None } else { Some(product_id) };
Expand All @@ -117,15 +118,21 @@ pub async fn update_subscription(
let is_upgrade_from_free = existing_tier.id == 1;
sqlx::query(
"UPDATE workspaces SET
tier_id = CASE
WHEN $3 THEN 1
ELSE (SELECT id FROM subscription_tiers WHERE stripe_product_id = $2)
END
WHERE id = $1",
tier_id = CASE
WHEN $3 THEN 1
ELSE (
SELECT id
FROM subscription_tiers
WHERE stripe_product_id = $2
)
END,
subscription_id = $4
WHERE id = $1",
)
.bind(workspace_id)
.bind(product_id)
.bind(cancel)
.bind(subscription_id)
.execute(pool)
.await?;
Ok(is_upgrade_from_free)
Expand Down
5 changes: 0 additions & 5 deletions app-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,11 +426,6 @@ fn main() -> anyhow::Result<()> {
.service(routes::subscriptions::save_stripe_customer_id)
.service(routes::subscriptions::get_user_subscription_info),
)
.service(
web::scope("/api/v1/users")
// No auth, Next JS backend will call this after verifying stripe's signature
.service(routes::users::get_user_from_stripe_customer_id),
)
.service(
web::scope("/api/v1/projects")
.wrap(auth)
Expand Down
1 change: 0 additions & 1 deletion app-server/src/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ pub mod provider_api_keys;
pub mod subscriptions;
pub mod traces;
pub mod types;
pub mod users;
pub mod workspace;

use serde::{Deserialize, Serialize};
Expand Down
8 changes: 5 additions & 3 deletions app-server/src/routes/subscriptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ pub async fn save_stripe_customer_id(
request: web::Json<SubscriptionRequest>,
user: User,
) -> ResponseResult {
if !is_feature_enabled(Feature::Storage) {
if !is_feature_enabled(Feature::Subscription) {
return Ok(HttpResponse::Forbidden().finish());
}
db::subscriptions::save_stripe_customer_id(&db.pool, &user.id, &request.stripe_customer_id)
Expand All @@ -51,7 +51,7 @@ pub async fn save_stripe_customer_id(

#[get("")] // GET /api/v1/subscriptions
pub async fn get_user_subscription_info(db: web::Data<db::DB>, user: User) -> ResponseResult {
if !is_feature_enabled(Feature::Storage) {
if !is_feature_enabled(Feature::Subscription) {
return Ok(HttpResponse::Forbidden().finish());
}
let stripe_customer_id =
Expand All @@ -72,6 +72,7 @@ struct ManageSubscriptionRequest {
pub quantity: Option<i64>,
#[serde(default)]
pub cancel: bool,
pub subscription_id: String,
}

#[post("")] // POST /api/v1/manage-subscription
Expand All @@ -94,14 +95,15 @@ pub async fn update_subscription(
db::subscriptions::activate_stripe_customer(&db.pool, &stripe_customer_id).await?;

if is_additional_seats && quantity > 0 {
db::subscriptions::add_seats(&db.pool, &workspace_id, quantity).await?;
db::subscriptions::set_seats(&db.pool, &workspace_id, quantity).await?;
return Ok(HttpResponse::Ok().finish());
}

let is_upgrade_from_free = db::subscriptions::update_subscription(
&db.pool,
&workspace_id,
&request.product_id,
&request.subscription_id,
request.cancel,
)
.await?;
Expand Down
20 changes: 0 additions & 20 deletions app-server/src/routes/users.rs

This file was deleted.

73 changes: 73 additions & 0 deletions frontend/app/api/workspaces/[workspaceId]/update-seats/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { db } from '@/lib/db/drizzle';
import { workspaces } from '@/lib/db/migrations/schema';
import { isCurrentUserMemberOfWorkspace } from '@/lib/db/utils';
import { eq } from 'drizzle-orm';
import { NextRequest } from 'next/server';
import stripe from 'stripe';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stripe import should be corrected to import Stripe from 'stripe'; and instantiated as const s = new Stripe(process.env.STRIPE_SECRET_KEY!);.

Suggested change
import stripe from 'stripe';
import Stripe from 'stripe';


const SEAT_PRICE_LOOKUP_KEY = 'additional_seat_2024_11';

export async function POST(
req: NextRequest,
{ params }: { params: { workspaceId: string } }
): Promise<Response> {
if (!isCurrentUserMemberOfWorkspace(params.workspaceId)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function isCurrentUserMemberOfWorkspace should be awaited if it returns a promise. This ensures proper authorization checks.

Suggested change
if (!isCurrentUserMemberOfWorkspace(params.workspaceId)) {
if (!(await isCurrentUserMemberOfWorkspace(params.workspaceId))) {

return Response.json({ error: 'Unauthorized' }, { status: 401 });
}

const s = new stripe(process.env.STRIPE_SECRET_KEY!);

const workspace = await db.query.workspaces.findFirst({
where: eq(workspaces.id, params.workspaceId),
with: {
subscriptionTier: {
columns: {
membersPerWorkspace: true
}
}
}
});

if (!workspace) {
return Response.json({ error: 'Workspace not found' }, { status: 404 });
}

if (!workspace.subscriptionId) {
return Response.json(
{ error: 'Cannot find subscription id for workspace' },
{ status: 403 }
);
}

const prices = await s.prices.list({
lookup_keys: [SEAT_PRICE_LOOKUP_KEY]
});
const priceId =
prices.data.find(p => p.lookup_key === SEAT_PRICE_LOOKUP_KEY)?.id!;

const subscriptionItems = await s.subscriptionItems.list({
subscription: workspace.subscriptionId
});
const existingItem = subscriptionItems.data.find(item => item.price.lookup_key === SEAT_PRICE_LOOKUP_KEY);

const existingSeats = existingItem ? existingItem.quantity : 0;
const body = await req.json();
const newQuantity = Math.max(0, body.quantity + existingSeats);

if (existingItem) {
await s.subscriptionItems.update(existingItem.id, {
price: priceId,
quantity: newQuantity,
proration_behavior: 'always_invoice',
});
} else {
await s.subscriptionItems.create({
subscription: workspace.subscriptionId,
price: priceId,
quantity: newQuantity,
proration_behavior: 'always_invoice',
});
}

return Response.json({ message: 'Seats bought' }, { status: 200 });
}
Loading