Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions backend/apps/cloud/src/project/entity/project.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ export class Project {
@Column('datetime', { nullable: true, default: null })
revenueLastSyncAt: Date | null

@Column('boolean', { default: false })
revenueApiEnabled: boolean

@ApiProperty()
@Column('varchar', { nullable: true, default: null, length: 512 })
websiteUrl: string | null
Expand Down
6 changes: 3 additions & 3 deletions backend/apps/cloud/src/project/project.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1996,7 +1996,7 @@ export class ProjectController {
@ApiOkResponse({ type: ProjectViewEntity })
@ApiBearerAuth()
@Post(':projectId/views')
@Auth()
@Auth(true)
async createProjectView(
@Param() params: ProjectIdDto,
@Body() body: CreateProjectViewDto,
Expand Down Expand Up @@ -2069,7 +2069,7 @@ export class ProjectController {
@ApiOkResponse({ type: ProjectViewEntity })
@ApiBearerAuth()
@Patch(':projectId/views/:viewId')
@Auth()
@Auth(true)
async updateProjectView(
@Param() params: ProjectViewIdsDto,
@Body() body: UpdateProjectViewDto,
Expand Down Expand Up @@ -2116,7 +2116,7 @@ export class ProjectController {
@ApiBearerAuth()
@HttpCode(HttpStatus.NO_CONTENT)
@Delete(':projectId/views/:viewId')
@Auth()
@Auth(true)
async deleteProjectView(
@Param() params: ProjectViewIdsDto,
@CurrentUserId() userId: string,
Expand Down
21 changes: 15 additions & 6 deletions backend/apps/cloud/src/revenue/dto/connect-revenue.dto.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,39 @@
import { ApiProperty } from '@nestjs/swagger'
import { IsNotEmpty, IsOptional, IsString, Length, IsIn } from 'class-validator'
import {
IsNotEmpty,
IsOptional,
IsString,
Length,
IsIn,
ValidateIf,
} from 'class-validator'

type RevenueProviderDto = 'stripe' | 'paddle'
type RevenueProviderDto = 'stripe' | 'paddle' | 'api'

export class ConnectRevenueDto {
@ApiProperty({
description: 'Revenue provider to connect',
enum: ['stripe', 'paddle'],
enum: ['stripe', 'paddle', 'api'],
example: 'stripe',
})
@IsNotEmpty()
@IsString()
@IsIn(['stripe', 'paddle'])
@IsIn(['stripe', 'paddle', 'api'])
provider: RevenueProviderDto

@ApiProperty({
description:
'API key for the selected provider. Stripe: rk_live_*. Paddle: pdl_live_*.',
'API key for the selected provider. Required for stripe (rk_live_*) and paddle (pdl_live_*). Not required for the "api" provider, which ingests revenue via POST /log/revenue.',
examples: {
stripe: { value: 'rk_live_xxxxxxxxxxxxxxxx' },
paddle: { value: 'pdl_live_xxxxxxxxxxxxxxxx' },
} as any,
required: false,
})
@ValidateIf((o: ConnectRevenueDto) => o.provider !== 'api')
@IsNotEmpty()
@IsString()
apiKey: string
apiKey?: string

@ApiProperty({
description: 'Currency code for revenue reporting (ISO 4217)',
Expand Down
153 changes: 153 additions & 0 deletions backend/apps/cloud/src/revenue/dto/log-revenue.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
import { Transform, Type } from 'class-transformer'
import {
IsIn,
IsISO8601,
IsNotEmpty,
IsNumber,
IsObject,
IsOptional,
IsPositive,
IsString,
Length,
Matches,
MaxLength,
Validate,
} from 'class-validator'

import { PID_REGEX } from '../../common/constants'
import {
MetadataKeysQuantity,
MetadataSizeLimit,
MetadataValueType,
MAX_METADATA_KEYS,
MAX_METADATA_VALUE_LENGTH,
transformMetadataJsonPrimitivesToString,
} from '../../analytics/dto/events.dto'

type LogRevenueType = 'sale' | 'refund' | 'subscription'

export class LogRevenueDto {
@ApiProperty({
example: 'aUn1quEid-3',
description: 'The project ID',
})
@IsNotEmpty()
@Matches(PID_REGEX, { message: 'The provided Project ID (pid) is incorrect' })
pid: string

@ApiPropertyOptional({
example: 'order_42891',
description:
'Stable unique identifier for this transaction. Re-sending with the same transactionId is idempotent (replaces the previous version). If omitted, a UUID is generated.',
maxLength: 256,
})
@IsOptional()
@IsString()
@MaxLength(256)
transactionId?: string

@ApiProperty({
enum: ['sale', 'refund', 'subscription'],
example: 'sale',
description:
'Transaction type. Use "refund" to record a refund (amount will be stored as a negative value).',
})
@IsNotEmpty()
@IsIn(['sale', 'refund', 'subscription'])
type: LogRevenueType

@ApiProperty({
example: 49.99,
description:
'Transaction amount in major currency units (e.g. 49.99 for $49.99). Always send a positive number; for refunds the sign is applied automatically.',
})
@Type(() => Number)
@IsNumber({ maxDecimalPlaces: 4 })
@IsPositive()
amount: number

@ApiProperty({
example: 'USD',
description: 'ISO 4217 currency code of the amount',
})
@IsNotEmpty()
@IsString()
@Length(3, 3)
@Transform(({ value }) =>
typeof value === 'string' ? value.toUpperCase() : value,
)
@Matches(/^[A-Z]{3}$/, {
message: 'currency must be a 3-letter ISO 4217 code',
})
currency: string
Comment thread
Blaumaus marked this conversation as resolved.

@ApiPropertyOptional({
example: 'sku_pro_yearly',
description: 'Optional product / SKU identifier',
maxLength: 256,
})
@IsOptional()
@IsString()
@MaxLength(256)
productId?: string

@ApiPropertyOptional({
example: 'Pro plan (yearly)',
description: 'Optional human-readable product name',
maxLength: 512,
})
@IsOptional()
@IsString()
@MaxLength(512)
productName?: string

@ApiPropertyOptional({
example: 'usr_12345',
description:
'Optional profile ID for revenue attribution. Same value as you would pass to the Swetrix tracking script.',
maxLength: 256,
})
@IsOptional()
@IsString()
@MaxLength(256)
profileId?: string

@ApiPropertyOptional({
example: '8214637194021987452',
description:
'Optional session ID for attributing revenue to a specific browsing session.',
maxLength: 256,
})
@IsOptional()
@IsString()
@MaxLength(256)
sessionId?: string

@ApiPropertyOptional({
example: '2026-04-27T16:32:01Z',
description:
'Transaction date as an ISO 8601 string. Defaults to the time the request is received.',
})
@IsOptional()
@IsISO8601()
created?: string

@ApiPropertyOptional({
example: { plan: 'pro', billing: 'annual' },
description: 'Arbitrary metadata stored alongside the transaction',
})
@IsOptional()
@IsObject()
@Validate(MetadataKeysQuantity, {
message: `Metadata object can't have more than ${MAX_METADATA_KEYS} keys`,
})
@Transform(({ value }) => transformMetadataJsonPrimitivesToString(value))
@Validate(MetadataValueType, {
message: 'All of metadata object values must be primitive JSON values',
})
@Validate(MetadataSizeLimit, {
message: `Metadata object can't have keys and values with total length more than ${MAX_METADATA_VALUE_LENGTH} characters`,
})
metadata?: Record<string, string>
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export enum RevenueProvider {
STRIPE = 'stripe',
PADDLE = 'paddle',
API = 'api',
}

export enum RevenueType {
Expand Down
Loading
Loading