Skip to content

Commit d4721f6

Browse files
committed
Merge main into subscription-client
2 parents 7a1531b + 2ef223e commit d4721f6

File tree

7 files changed

+80
-71
lines changed

7 files changed

+80
-71
lines changed

cli/src/components/terminal-command-display.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ interface TerminalCommandDisplayProps {
2020
cwd?: string
2121
/** Timeout in seconds for the command */
2222
timeoutSeconds?: number
23+
/** Optional width override for wrapping calculations */
24+
availableWidth?: number
2325
}
2426

2527
/**
@@ -33,10 +35,10 @@ export const TerminalCommandDisplay = ({
3335
maxVisibleLines,
3436
isRunning = false,
3537
timeoutSeconds,
38+
availableWidth,
3639
}: TerminalCommandDisplayProps) => {
3740
const theme = useTheme()
38-
const { contentMaxWidth } = useTerminalDimensions()
39-
const padding = 5
41+
const { separatorWidth } = useTerminalDimensions()
4042
const [isExpanded, setIsExpanded] = useState(false)
4143

4244
// Default max lines depends on whether expandable
@@ -77,7 +79,7 @@ export const TerminalCommandDisplay = ({
7779
}
7880

7981
// With output - calculate visual lines
80-
const width = Math.max(10, contentMaxWidth - padding * 2)
82+
const width = Math.max(10, availableWidth ?? separatorWidth)
8183
const allLines = output.split('\n')
8284

8385
// Calculate total visual lines across all output lines

cli/src/components/tools/run-terminal-command.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export const parseTerminalOutput = (rawOutput: string | undefined): ParsedTermin
4949
export const RunTerminalCommandComponent = defineToolComponent({
5050
toolName: 'run_terminal_command',
5151

52-
render(toolBlock): ToolRenderConfig {
52+
render(toolBlock, _theme, options): ToolRenderConfig {
5353
// Extract command and timeout from input
5454
const input = toolBlock.input as { command?: string; timeout_seconds?: number } | undefined
5555
const command = typeof input?.command === 'string' ? input.command.trim() : ''
@@ -67,6 +67,7 @@ export const RunTerminalCommandComponent = defineToolComponent({
6767
maxVisibleLines={5}
6868
cwd={startingCwd}
6969
timeoutSeconds={timeoutSeconds}
70+
availableWidth={options.availableWidth}
7071
/>
7172
)
7273

cli/src/components/usage-banner.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export const UsageBanner = ({ showTime }: { showTime: number }) => {
157157
)}
158158
</box>
159159
{/* See more link */}
160-
<text style={{ fg: theme.muted }}>See more on codebuff.com</text>
160+
<text style={{ fg: theme.muted }}>See more on {WEBSITE_URL}</text>
161161
</box>
162162
</Button>
163163

web/src/app/api/stripe/create-subscription/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export async function POST(req: NextRequest) {
7272
const checkoutSession = await stripeServer.checkout.sessions.create({
7373
customer: user.stripe_customer_id,
7474
mode: 'subscription',
75+
invoice_creation: { enabled: true },
7576
tax_id_collection: { enabled: true }, // optional (EU B2B)
7677
customer_update: { name: "auto", address: "auto" },
7778
line_items: [{ price: priceId, quantity: 1 }],

web/src/app/api/stripe/webhook/__tests__/org-billing-events.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ const setupMocks = async () => {
3838
}))
3939

4040
// Import after mocking
41-
const webhookModule = await import('../route')
42-
isOrgBillingEvent = webhookModule.isOrgBillingEvent
43-
isOrgCustomer = webhookModule.isOrgCustomer
41+
const helpersModule = await import('../_helpers')
42+
isOrgBillingEvent = helpersModule.isOrgBillingEvent
43+
isOrgCustomer = helpersModule.isOrgCustomer
4444
}
4545

4646
// Setup mocks at module load time (following ban-conditions.test.ts pattern)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import db from '@codebuff/internal/db'
2+
import * as schema from '@codebuff/internal/db/schema'
3+
import { eq } from 'drizzle-orm'
4+
5+
import type Stripe from 'stripe'
6+
7+
import { logger } from '@/util/logger'
8+
9+
/**
10+
* Checks whether a Stripe customer ID belongs to an organization.
11+
*
12+
* Uses `org.stripe_customer_id` which is set at org creation time, making it
13+
* reliable regardless of webhook ordering (unlike `stripe_subscription_id`
14+
* which may not be populated yet when early invoice events arrive).
15+
*/
16+
export async function isOrgCustomer(stripeCustomerId: string): Promise<boolean> {
17+
try {
18+
const orgs = await db
19+
.select({ id: schema.org.id })
20+
.from(schema.org)
21+
.where(eq(schema.org.stripe_customer_id, stripeCustomerId))
22+
.limit(1)
23+
return orgs.length > 0
24+
} catch (error) {
25+
logger.error(
26+
{ stripeCustomerId, error },
27+
'Failed to check if customer is an org - defaulting to false',
28+
)
29+
return false
30+
}
31+
}
32+
33+
/**
34+
* BILLING_DISABLED: Checks if a Stripe event is related to organization billing.
35+
* Used to reject org billing events while keeping personal billing working.
36+
*/
37+
export async function isOrgBillingEvent(event: Stripe.Event): Promise<boolean> {
38+
const eventData = event.data.object as unknown as Record<string, unknown>
39+
const metadata = (eventData.metadata || {}) as Record<string, string>
40+
41+
// Check metadata for organization markers
42+
if (metadata.organization_id || metadata.organizationId) {
43+
return true
44+
}
45+
if (metadata.grantType === 'organization_purchase') {
46+
return true
47+
}
48+
49+
// For invoice events, check if customer belongs to an org
50+
// (metadata.organizationId is already checked above in the generic metadata check)
51+
if (event.type.startsWith('invoice.')) {
52+
const customerId = eventData.customer
53+
if (customerId && typeof customerId === 'string') {
54+
return await isOrgCustomer(customerId)
55+
}
56+
}
57+
58+
// For subscription events, check if customer is an org
59+
if (event.type.startsWith('customer.subscription.')) {
60+
const customerId = eventData.customer
61+
if (customerId && typeof customerId === 'string') {
62+
return await isOrgCustomer(customerId)
63+
}
64+
}
65+
66+
return false
67+
}

web/src/app/api/stripe/webhook/route.ts

Lines changed: 1 addition & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -27,66 +27,7 @@ import {
2727
} from '@/lib/ban-conditions'
2828
import { ORG_BILLING_ENABLED } from '@/lib/billing-config'
2929
import { logger } from '@/util/logger'
30-
31-
/**
32-
* Checks whether a Stripe customer ID belongs to an organization.
33-
*
34-
* Uses `org.stripe_customer_id` which is set at org creation time, making it
35-
* reliable regardless of webhook ordering (unlike `stripe_subscription_id`
36-
* which may not be populated yet when early invoice events arrive).
37-
*/
38-
async function isOrgCustomer(stripeCustomerId: string): Promise<boolean> {
39-
try {
40-
const orgs = await db
41-
.select({ id: schema.org.id })
42-
.from(schema.org)
43-
.where(eq(schema.org.stripe_customer_id, stripeCustomerId))
44-
.limit(1)
45-
return orgs.length > 0
46-
} catch (error) {
47-
logger.error(
48-
{ stripeCustomerId, error },
49-
'Failed to check if customer is an org - defaulting to false',
50-
)
51-
return false
52-
}
53-
}
54-
55-
/**
56-
* BILLING_DISABLED: Checks if a Stripe event is related to organization billing.
57-
* Used to reject org billing events while keeping personal billing working.
58-
*/
59-
async function isOrgBillingEvent(event: Stripe.Event): Promise<boolean> {
60-
const eventData = event.data.object as unknown as Record<string, unknown>
61-
const metadata = (eventData.metadata || {}) as Record<string, string>
62-
63-
// Check metadata for organization markers
64-
if (metadata.organization_id || metadata.organizationId) {
65-
return true
66-
}
67-
if (metadata.grantType === 'organization_purchase') {
68-
return true
69-
}
70-
71-
// For invoice events, check if customer belongs to an org
72-
// (metadata.organizationId is already checked above in the generic metadata check)
73-
if (event.type.startsWith('invoice.')) {
74-
const customerId = eventData.customer
75-
if (customerId && typeof customerId === 'string') {
76-
return await isOrgCustomer(customerId)
77-
}
78-
}
79-
80-
// For subscription events, check if customer is an org
81-
if (event.type.startsWith('customer.subscription.')) {
82-
const customerId = eventData.customer
83-
if (customerId && typeof customerId === 'string') {
84-
return await isOrgCustomer(customerId)
85-
}
86-
}
87-
88-
return false
89-
}
30+
import { isOrgBillingEvent, isOrgCustomer } from './_helpers'
9031

9132
async function handleCheckoutSessionCompleted(
9233
session: Stripe.Checkout.Session,
@@ -699,6 +640,3 @@ const webhookHandler = async (req: NextRequest): Promise<NextResponse> => {
699640
}
700641

701642
export { webhookHandler as POST }
702-
703-
// Exported for testing
704-
export { isOrgBillingEvent, isOrgCustomer }

0 commit comments

Comments
 (0)