Compare commits

..

2 Commits

9 changed files with 73 additions and 84 deletions

View File

@ -1,4 +1,4 @@
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } 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: 'invoice_items', schema: 'public' }) @Entity({ name: 'billing_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: 'numeric', precision: 12, scale: 4 }) @Column({ type: 'bigint', default: 1 })
quantity!: number; // 1 for subscription, 2.5 for 2.5M tokens overage quantity!: number; // 1 for subscription, token count for overage
@Column({ name: 'unit_price', type: 'int' }) @Column({ name: 'unit_price', type: 'int' })
unitPrice!: number; // in cents/fen unitPrice!: number; // in cents/fen
@ -33,4 +33,7 @@ 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,5 +1,4 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, OneToMany } from 'typeorm'; import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
import { InvoiceItem } from './invoice-item.entity';
export const InvoiceStatus = { export const InvoiceStatus = {
DRAFT: 'draft', DRAFT: 'draft',
@ -17,7 +16,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: 'invoices', schema: 'public' }) @Entity({ name: 'billing_invoices', schema: 'public' })
export class Invoice { export class Invoice {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id!: string; id!: string;
@ -61,9 +60,9 @@ export class Invoice {
@Column({ name: 'paid_at', type: 'timestamptz', nullable: true }) @Column({ name: 'paid_at', type: 'timestamptz', nullable: true })
paidAt?: Date; paidAt?: Date;
@OneToMany(() => InvoiceItem, (item) => item.invoiceId, { eager: false }) @CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
items?: InvoiceItem[];
@CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date; createdAt!: Date;
@Column({ name: 'updated_at', type: 'timestamptz', nullable: true })
updatedAt?: 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: 'payment_methods', schema: 'public' }) @Entity({ name: 'billing_payment_methods', schema: 'public' })
export class PaymentMethod { export class PaymentMethod {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id!: string; id!: string;
@Column({ name: 'tenant_id', type: 'varchar', length: 100 }) @Column({ name: 'tenant_id', type: 'uuid' })
tenantId!: string; tenantId!: string;
@Column({ type: 'varchar', length: 20 }) @Column({ type: 'varchar', length: 20 })
@ -14,24 +14,21 @@ 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', default: '{}' }) @Column({ type: 'jsonb', nullable: true })
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: 200, nullable: true }) @Column({ name: 'provider_customer_id', type: 'varchar', length: 255, 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({ type: 'timestamptz' }) @CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt!: Date; createdAt!: Date;
@UpdateDateColumn({ type: 'timestamptz' }) @UpdateDateColumn({ name: 'updated_at', 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: 'payments', schema: 'public' }) @Entity({ name: 'billing_payments', schema: 'public' })
export class Payment { export class Payment {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id!: string; id!: string;
@Column({ type: 'varchar', length: 100 }) @Column({ name: 'tenant_id', type: 'uuid' })
tenantId!: string; tenantId!: string;
@Column({ type: 'uuid', nullable: true }) @Column({ name: 'invoice_id', type: 'uuid' })
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,55 +1,49 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity({ name: 'plans', schema: 'public' }) @Entity({ name: 'billing_plans', schema: 'public' })
export class Plan { export class Plan {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id!: string; id!: string;
@Column({ type: 'varchar', length: 30, unique: true }) @Column({ type: 'varchar', length: 50, unique: true })
name!: string; // 'free' | 'pro' | 'enterprise' name!: string; // 'free' | 'pro' | 'enterprise'
@Column({ type: 'varchar', length: 100 }) @Column({ name: 'display_name', type: 'varchar', length: 100 })
displayName!: string; displayName!: string;
@Column({ type: 'int', default: 0 }) @Column({ name: 'monthly_price_usd_cents', type: 'int', default: 0 })
monthlyPriceCentsUsd!: number; // e.g. 4999 = $49.99 monthlyPriceCentsUsd!: number; // e.g. 4999 = $49.99
@Column({ type: 'int', default: 0 }) @Column({ name: 'monthly_price_cny', type: 'int', default: 0 })
monthlyPriceFenCny!: number; // e.g. 34900 = ¥349.00 monthlyPriceFenCny!: number; // e.g. 34900 = ¥349.00
@Column({ type: 'bigint', default: 100000 }) @Column({ name: 'included_tokens_per_month', type: 'bigint', default: 100000 })
includedTokens!: number; // tokens per month included in plan includedTokens!: number; // tokens per month included in plan
@Column({ type: 'int', default: 0 }) @Column({ name: 'overage_rate_cents_per_m_token', type: 'int', default: 0 })
overageRateCentsPerMTokenUsd!: number; // price per 1M overage tokens in cents overageRateCentsPerMTokenUsd!: number; // price per 1M overage tokens in cents
@Column({ type: 'int', default: 5 }) @Column({ name: 'max_servers', type: 'int', default: 5 })
maxServers!: number; // -1 = unlimited maxServers!: number; // -1 = unlimited
@Column({ type: 'int', default: 3 }) @Column({ name: 'max_users', type: 'int', default: 3 })
maxUsers!: number; // -1 = unlimited maxUsers!: number; // -1 = unlimited
@Column({ type: 'int', default: 10 }) @Column({ name: 'max_standing_orders', type: 'int', default: 10 })
maxStandingOrders!: number; // -1 = unlimited maxStandingOrders!: number; // -1 = unlimited
@Column({ type: 'int', default: 100 }) @Column({ name: 'hard_limit_percent', 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({ type: 'jsonb', default: '{}' }) @Column({ name: 'trial_days', type: 'int', default: 0 })
features!: Record<string, boolean>;
@Column({ type: 'boolean', default: true })
isActive!: boolean;
@Column({ type: 'int', default: 0 })
sortOrder!: number;
@Column({ type: 'int', default: 14 })
trialDays!: number; trialDays!: number;
@CreateDateColumn({ type: 'timestamptz' }) @Column({ name: 'is_active', type: 'boolean', default: true })
isActive!: boolean;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt!: Date; createdAt!: Date;
@UpdateDateColumn({ type: 'timestamptz' }) @UpdateDateColumn({ name: 'updated_at', 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: 'subscriptions', schema: 'public' }) @Entity({ name: 'billing_subscriptions', schema: 'public' })
export class Subscription { export class Subscription {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id!: string; id!: string;
@Column({ type: 'varchar', length: 100 }) @Column({ name: 'tenant_id', type: 'uuid' })
tenantId!: string; tenantId!: string;
@Column({ type: 'uuid' }) @Column({ name: 'plan_id', 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({ type: 'timestamptz' }) @Column({ name: 'current_period_start', type: 'timestamptz' })
currentPeriodStart!: Date; currentPeriodStart!: Date;
@Column({ type: 'timestamptz' }) @Column({ name: 'current_period_end', type: 'timestamptz' })
currentPeriodEnd!: Date; currentPeriodEnd!: Date;
@Column({ type: 'timestamptz', nullable: true }) @Column({ name: 'trial_ends_at', type: 'timestamptz', nullable: true })
trialEndsAt?: Date; trialEndsAt?: Date;
@Column({ type: 'timestamptz', nullable: true }) @Column({ name: 'cancelled_at', type: 'timestamptz', nullable: true })
cancelledAt?: Date; cancelledAt?: Date;
@Column({ type: 'boolean', default: false }) @Column({ name: 'cancel_at_period_end', type: 'boolean', default: false })
cancelAtPeriodEnd!: boolean; cancelAtPeriodEnd!: boolean;
@Column({ type: 'uuid', nullable: true }) @Column({ name: 'next_plan_id', type: 'uuid', nullable: true })
nextPlanId?: string; // for scheduled downgrades nextPlanId?: string; // for scheduled downgrades
@CreateDateColumn({ type: 'timestamptz' }) @CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt!: Date; createdAt!: Date;
@UpdateDateColumn({ type: 'timestamptz' }) @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt!: Date; updatedAt!: Date;
} }

View File

@ -1,11 +1,11 @@
import { Entity, PrimaryGeneratedColumn, Column, UpdateDateColumn } from 'typeorm'; import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity({ name: 'usage_aggregates', schema: 'public' }) @Entity({ name: 'billing_usage_aggregates', schema: 'public' })
export class UsageAggregate { export class UsageAggregate {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id!: string; id!: string;
@Column({ name: 'tenant_id', type: 'varchar', length: 100 }) @Column({ name: 'tenant_id', type: 'uuid' })
tenantId!: string; tenantId!: string;
@Column({ type: 'int' }) @Column({ type: 'int' })
@ -29,12 +29,15 @@ 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: 14, scale: 6, default: 0 }) @Column({ name: 'total_cost_usd', type: 'numeric', precision: 12, 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;
@UpdateDateColumn({ type: 'timestamptz' }) @CreateDateColumn({ name: 'created_at', 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',
monthlyPriceUsdCents: 0, monthlyPriceCentsUsd: 0,
monthlyPriceCny: 0, monthlyPriceFenCny: 0,
includedTokensPerMonth: 100_000, includedTokens: 100_000,
overageRateCentsPerMToken: 0, overageRateCentsPerMTokenUsd: 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',
monthlyPriceUsdCents: 4999, // $49.99 monthlyPriceCentsUsd: 4999, // $49.99
monthlyPriceCny: 34900, // ¥349.00 monthlyPriceFenCny: 34900, // ¥349.00
includedTokensPerMonth: 1_000_000, includedTokens: 1_000_000,
overageRateCentsPerMToken: 800, // $8.00 per MTok overageRateCentsPerMTokenUsd: 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',
monthlyPriceUsdCents: 19999, // $199.99 monthlyPriceCentsUsd: 19999, // $199.99
monthlyPriceCny: 139900, // ¥1399.00 monthlyPriceFenCny: 139900, // ¥1399.00
includedTokensPerMonth: 10_000_000, includedTokens: 10_000_000,
overageRateCentsPerMToken: 500, // $5.00 per MTok overageRateCentsPerMTokenUsd: 500, // $5.00 per MTok
maxServers: -1, maxServers: -1,
maxUsers: -1, maxUsers: -1,
maxStandingOrders: -1, maxStandingOrders: -1,

View File

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