Compare commits

..

No commits in common. "f0634c2e49720230e9e0e70ce31f97019ec1f7d7" and "d96ea918157208ab1a28982a1a28ade575327351" have entirely different histories.

9 changed files with 83 additions and 72 deletions

View File

@ -1,4 +1,4 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm'; import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
export const InvoiceItemType = { export const InvoiceItemType = {
SUBSCRIPTION: 'subscription', SUBSCRIPTION: 'subscription',
@ -8,7 +8,7 @@ export const InvoiceItemType = {
} as const; } as const;
export type InvoiceItemType = typeof InvoiceItemType[keyof typeof InvoiceItemType]; export type InvoiceItemType = typeof InvoiceItemType[keyof typeof InvoiceItemType];
@Entity({ name: 'billing_invoice_items', schema: 'public' }) @Entity({ name: 'invoice_items', schema: 'public' })
export class InvoiceItem { export class InvoiceItem {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id!: string; id!: string;
@ -22,8 +22,8 @@ export class InvoiceItem {
@Column({ name: 'item_type', type: 'varchar', length: 30 }) @Column({ name: 'item_type', type: 'varchar', length: 30 })
itemType!: InvoiceItemType; // 'subscription' | 'overage' | 'credit' | 'adjustment' itemType!: InvoiceItemType; // 'subscription' | 'overage' | 'credit' | 'adjustment'
@Column({ type: 'bigint', default: 1 }) @Column({ type: 'numeric', precision: 12, scale: 4 })
quantity!: number; // 1 for subscription, token count for overage quantity!: number; // 1 for subscription, 2.5 for 2.5M tokens overage
@Column({ name: 'unit_price', type: 'int' }) @Column({ name: 'unit_price', type: 'int' })
unitPrice!: number; // in cents/fen unitPrice!: number; // in cents/fen
@ -33,7 +33,4 @@ export class InvoiceItem {
@Column({ type: 'varchar', length: 3, default: 'USD' }) @Column({ type: 'varchar', length: 3, default: 'USD' })
currency!: string; currency!: string;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt!: Date;
} }

View File

@ -1,4 +1,5 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm'; import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, OneToMany } from 'typeorm';
import { InvoiceItem } from './invoice-item.entity';
export const InvoiceStatus = { export const InvoiceStatus = {
DRAFT: 'draft', DRAFT: 'draft',
@ -16,7 +17,7 @@ export const InvoiceCurrency = {
} as const; } as const;
export type InvoiceCurrency = typeof InvoiceCurrency[keyof typeof InvoiceCurrency]; export type InvoiceCurrency = typeof InvoiceCurrency[keyof typeof InvoiceCurrency];
@Entity({ name: 'billing_invoices', schema: 'public' }) @Entity({ name: 'invoices', schema: 'public' })
export class Invoice { export class Invoice {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id!: string; id!: string;
@ -60,9 +61,9 @@ export class Invoice {
@Column({ name: 'paid_at', type: 'timestamptz', nullable: true }) @Column({ name: 'paid_at', type: 'timestamptz', nullable: true })
paidAt?: Date; paidAt?: Date;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) @OneToMany(() => InvoiceItem, (item) => item.invoiceId, { eager: false })
createdAt!: Date; items?: InvoiceItem[];
@Column({ name: 'updated_at', type: 'timestamptz', nullable: true }) @CreateDateColumn({ type: 'timestamptz' })
updatedAt?: Date; createdAt!: Date;
} }

View File

@ -1,11 +1,11 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity({ name: 'billing_payment_methods', schema: 'public' }) @Entity({ name: 'payment_methods', schema: 'public' })
export class PaymentMethod { export class PaymentMethod {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id!: string; id!: string;
@Column({ name: 'tenant_id', type: 'uuid' }) @Column({ name: 'tenant_id', type: 'varchar', length: 100 })
tenantId!: string; tenantId!: string;
@Column({ type: 'varchar', length: 20 }) @Column({ type: 'varchar', length: 20 })
@ -14,21 +14,24 @@ export class PaymentMethod {
@Column({ name: 'display_name', type: 'varchar', length: 200, default: '' }) @Column({ name: 'display_name', type: 'varchar', length: 200, default: '' })
displayName!: string; // e.g. 'Visa *4242', 'Alipay xxx@qq.com' displayName!: string; // e.g. 'Visa *4242', 'Alipay xxx@qq.com'
@Column({ type: 'varchar', length: 30, nullable: true })
type?: string; // 'card' | 'alipay_account' | 'wechat_openid' | 'crypto_wallet'
@Column({ name: 'is_default', type: 'boolean', default: false }) @Column({ name: 'is_default', type: 'boolean', default: false })
isDefault!: boolean; isDefault!: boolean;
@Column({ type: 'jsonb', nullable: true }) @Column({ type: 'jsonb', default: '{}' })
details?: Record<string, any>; // { last4: '4242', brand: 'visa' } — NEVER store full card numbers details!: Record<string, any>; // { last4: '4242', brand: 'visa' } — NEVER store full card numbers
@Column({ name: 'provider_customer_id', type: 'varchar', length: 255, nullable: true }) @Column({ name: 'provider_customer_id', type: 'varchar', length: 200, nullable: true })
providerCustomerId?: string; providerCustomerId?: string;
@Column({ name: 'expires_at', type: 'timestamptz', nullable: true }) @Column({ name: 'expires_at', type: 'timestamptz', nullable: true })
expiresAt?: Date; expiresAt?: Date;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) @CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date; createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) @UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: Date; updatedAt!: Date;
} }

View File

@ -9,16 +9,16 @@ export const PaymentStatus = {
} as const; } as const;
export type PaymentStatus = typeof PaymentStatus[keyof typeof PaymentStatus]; export type PaymentStatus = typeof PaymentStatus[keyof typeof PaymentStatus];
@Entity({ name: 'billing_payments', schema: 'public' }) @Entity({ name: 'payments', schema: 'public' })
export class Payment { export class Payment {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id!: string; id!: string;
@Column({ name: 'tenant_id', type: 'uuid' }) @Column({ type: 'varchar', length: 100 })
tenantId!: string; tenantId!: string;
@Column({ name: 'invoice_id', type: 'uuid' }) @Column({ type: 'uuid', nullable: true })
invoiceId!: string; invoiceId?: string;
@Column({ type: 'varchar', length: 20 }) @Column({ type: 'varchar', length: 20 })
provider!: string; // 'stripe' | 'alipay' | 'wechat_pay' | 'crypto' provider!: string; // 'stripe' | 'alipay' | 'wechat_pay' | 'crypto'

View File

@ -1,49 +1,55 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity({ name: 'billing_plans', schema: 'public' }) @Entity({ name: 'plans', schema: 'public' })
export class Plan { export class Plan {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id!: string; id!: string;
@Column({ type: 'varchar', length: 50, unique: true }) @Column({ type: 'varchar', length: 30, unique: true })
name!: string; // 'free' | 'pro' | 'enterprise' name!: string; // 'free' | 'pro' | 'enterprise'
@Column({ name: 'display_name', type: 'varchar', length: 100 }) @Column({ type: 'varchar', length: 100 })
displayName!: string; displayName!: string;
@Column({ name: 'monthly_price_usd_cents', type: 'int', default: 0 }) @Column({ type: 'int', default: 0 })
monthlyPriceCentsUsd!: number; // e.g. 4999 = $49.99 monthlyPriceCentsUsd!: number; // e.g. 4999 = $49.99
@Column({ name: 'monthly_price_cny', type: 'int', default: 0 }) @Column({ type: 'int', default: 0 })
monthlyPriceFenCny!: number; // e.g. 34900 = ¥349.00 monthlyPriceFenCny!: number; // e.g. 34900 = ¥349.00
@Column({ name: 'included_tokens_per_month', type: 'bigint', default: 100000 }) @Column({ type: 'bigint', default: 100000 })
includedTokens!: number; // tokens per month included in plan includedTokens!: number; // tokens per month included in plan
@Column({ name: 'overage_rate_cents_per_m_token', type: 'int', default: 0 }) @Column({ type: 'int', default: 0 })
overageRateCentsPerMTokenUsd!: number; // price per 1M overage tokens in cents overageRateCentsPerMTokenUsd!: number; // price per 1M overage tokens in cents
@Column({ name: 'max_servers', type: 'int', default: 5 }) @Column({ type: 'int', default: 5 })
maxServers!: number; // -1 = unlimited maxServers!: number; // -1 = unlimited
@Column({ name: 'max_users', type: 'int', default: 3 }) @Column({ type: 'int', default: 3 })
maxUsers!: number; // -1 = unlimited maxUsers!: number; // -1 = unlimited
@Column({ name: 'max_standing_orders', type: 'int', default: 10 }) @Column({ type: 'int', default: 10 })
maxStandingOrders!: number; // -1 = unlimited maxStandingOrders!: number; // -1 = unlimited
@Column({ name: 'hard_limit_percent', type: 'int', default: 100 }) @Column({ type: 'int', default: 100 })
hardLimitPercent!: number; // 100 = block at 100%, 150 = block at 150%, 0 = no limit hardLimitPercent!: number; // 100 = block at 100%, 150 = block at 150%, 0 = no limit
@Column({ name: 'trial_days', type: 'int', default: 0 }) @Column({ type: 'jsonb', default: '{}' })
trialDays!: number; features!: Record<string, boolean>;
@Column({ name: 'is_active', type: 'boolean', default: true }) @Column({ type: 'boolean', default: true })
isActive!: boolean; isActive!: boolean;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) @Column({ type: 'int', default: 0 })
sortOrder!: number;
@Column({ type: 'int', default: 14 })
trialDays!: number;
@CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date; createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) @UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: Date; updatedAt!: Date;
} }

View File

@ -9,41 +9,41 @@ export const SubscriptionStatus = {
} as const; } as const;
export type SubscriptionStatus = typeof SubscriptionStatus[keyof typeof SubscriptionStatus]; export type SubscriptionStatus = typeof SubscriptionStatus[keyof typeof SubscriptionStatus];
@Entity({ name: 'billing_subscriptions', schema: 'public' }) @Entity({ name: 'subscriptions', schema: 'public' })
export class Subscription { export class Subscription {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id!: string; id!: string;
@Column({ name: 'tenant_id', type: 'uuid' }) @Column({ type: 'varchar', length: 100 })
tenantId!: string; tenantId!: string;
@Column({ name: 'plan_id', type: 'uuid' }) @Column({ type: 'uuid' })
planId!: string; planId!: string;
@Column({ type: 'varchar', length: 20, default: 'trialing' }) @Column({ type: 'varchar', length: 20, default: 'trialing' })
status!: SubscriptionStatus; status!: SubscriptionStatus;
@Column({ name: 'current_period_start', type: 'timestamptz' }) @Column({ type: 'timestamptz' })
currentPeriodStart!: Date; currentPeriodStart!: Date;
@Column({ name: 'current_period_end', type: 'timestamptz' }) @Column({ type: 'timestamptz' })
currentPeriodEnd!: Date; currentPeriodEnd!: Date;
@Column({ name: 'trial_ends_at', type: 'timestamptz', nullable: true }) @Column({ type: 'timestamptz', nullable: true })
trialEndsAt?: Date; trialEndsAt?: Date;
@Column({ name: 'cancelled_at', type: 'timestamptz', nullable: true }) @Column({ type: 'timestamptz', nullable: true })
cancelledAt?: Date; cancelledAt?: Date;
@Column({ name: 'cancel_at_period_end', type: 'boolean', default: false }) @Column({ type: 'boolean', default: false })
cancelAtPeriodEnd!: boolean; cancelAtPeriodEnd!: boolean;
@Column({ name: 'next_plan_id', type: 'uuid', nullable: true }) @Column({ type: 'uuid', nullable: true })
nextPlanId?: string; // for scheduled downgrades nextPlanId?: string; // for scheduled downgrades
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) @CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date; createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) @UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: Date; updatedAt!: Date;
} }

View File

@ -1,11 +1,11 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; import { Entity, PrimaryGeneratedColumn, Column, UpdateDateColumn } from 'typeorm';
@Entity({ name: 'billing_usage_aggregates', schema: 'public' }) @Entity({ name: 'usage_aggregates', schema: 'public' })
export class UsageAggregate { export class UsageAggregate {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id!: string; id!: string;
@Column({ name: 'tenant_id', type: 'uuid' }) @Column({ name: 'tenant_id', type: 'varchar', length: 100 })
tenantId!: string; tenantId!: string;
@Column({ type: 'int' }) @Column({ type: 'int' })
@ -29,15 +29,12 @@ export class UsageAggregate {
@Column({ name: 'total_tokens', type: 'bigint', default: 0 }) @Column({ name: 'total_tokens', type: 'bigint', default: 0 })
totalTokens!: number; totalTokens!: number;
@Column({ name: 'total_cost_usd', type: 'numeric', precision: 12, scale: 6, default: 0 }) @Column({ name: 'total_cost_usd', type: 'numeric', precision: 14, scale: 6, default: 0 })
totalCostUsd!: number; totalCostUsd!: number;
@Column({ name: 'task_count', type: 'int', default: 0 }) @Column({ name: 'task_count', type: 'int', default: 0 })
taskCount!: number; taskCount!: number;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) @UpdateDateColumn({ type: 'timestamptz' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt!: Date; updatedAt!: Date;
} }

View File

@ -5,10 +5,10 @@ const SEED_PLANS = [
{ {
name: 'free', name: 'free',
displayName: 'Free', displayName: 'Free',
monthlyPriceCentsUsd: 0, monthlyPriceUsdCents: 0,
monthlyPriceFenCny: 0, monthlyPriceCny: 0,
includedTokens: 100_000, includedTokensPerMonth: 100_000,
overageRateCentsPerMTokenUsd: 0, overageRateCentsPerMToken: 0,
maxServers: 5, maxServers: 5,
maxUsers: 3, maxUsers: 3,
maxStandingOrders: 10, maxStandingOrders: 10,
@ -19,10 +19,10 @@ const SEED_PLANS = [
{ {
name: 'pro', name: 'pro',
displayName: 'Pro', displayName: 'Pro',
monthlyPriceCentsUsd: 4999, // $49.99 monthlyPriceUsdCents: 4999, // $49.99
monthlyPriceFenCny: 34900, // ¥349.00 monthlyPriceCny: 34900, // ¥349.00
includedTokens: 1_000_000, includedTokensPerMonth: 1_000_000,
overageRateCentsPerMTokenUsd: 800, // $8.00 per MTok overageRateCentsPerMToken: 800, // $8.00 per MTok
maxServers: 50, maxServers: 50,
maxUsers: 20, maxUsers: 20,
maxStandingOrders: 100, maxStandingOrders: 100,
@ -33,10 +33,10 @@ const SEED_PLANS = [
{ {
name: 'enterprise', name: 'enterprise',
displayName: 'Enterprise', displayName: 'Enterprise',
monthlyPriceCentsUsd: 19999, // $199.99 monthlyPriceUsdCents: 19999, // $199.99
monthlyPriceFenCny: 139900, // ¥1399.00 monthlyPriceCny: 139900, // ¥1399.00
includedTokens: 10_000_000, includedTokensPerMonth: 10_000_000,
overageRateCentsPerMTokenUsd: 500, // $5.00 per MTok overageRateCentsPerMToken: 500, // $5.00 per MTok
maxServers: -1, maxServers: -1,
maxUsers: -1, maxUsers: -1,
maxStandingOrders: -1, maxStandingOrders: -1,

View File

@ -62,7 +62,14 @@ export class InvoiceController {
periodEnd: invoice.periodEnd, periodEnd: invoice.periodEnd,
dueDate: invoice.dueDate, dueDate: invoice.dueDate,
paidAt: invoice.paidAt, paidAt: invoice.paidAt,
items: [], items: (invoice.items ?? []).map((item) => ({
id: item.id,
itemType: item.itemType,
description: item.description,
quantity: item.quantity,
unitPrice: item.unitPrice / 100,
amount: item.amount / 100,
})),
}; };
} }