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 = {
SUBSCRIPTION: 'subscription',
@ -8,7 +8,7 @@ export const InvoiceItemType = {
} as const;
export type InvoiceItemType = typeof InvoiceItemType[keyof typeof InvoiceItemType];
@Entity({ name: 'invoice_items', schema: 'public' })
@Entity({ name: 'billing_invoice_items', schema: 'public' })
export class InvoiceItem {
@PrimaryGeneratedColumn('uuid')
id!: string;
@ -22,8 +22,8 @@ export class InvoiceItem {
@Column({ name: 'item_type', type: 'varchar', length: 30 })
itemType!: InvoiceItemType; // 'subscription' | 'overage' | 'credit' | 'adjustment'
@Column({ type: 'numeric', precision: 12, scale: 4 })
quantity!: number; // 1 for subscription, 2.5 for 2.5M tokens overage
@Column({ type: 'bigint', default: 1 })
quantity!: number; // 1 for subscription, token count for overage
@Column({ name: 'unit_price', type: 'int' })
unitPrice!: number; // in cents/fen
@ -33,4 +33,7 @@ export class InvoiceItem {
@Column({ type: 'varchar', length: 3, default: 'USD' })
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 { InvoiceItem } from './invoice-item.entity';
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
export const InvoiceStatus = {
DRAFT: 'draft',
@ -17,7 +16,7 @@ export const InvoiceCurrency = {
} as const;
export type InvoiceCurrency = typeof InvoiceCurrency[keyof typeof InvoiceCurrency];
@Entity({ name: 'invoices', schema: 'public' })
@Entity({ name: 'billing_invoices', schema: 'public' })
export class Invoice {
@PrimaryGeneratedColumn('uuid')
id!: string;
@ -61,9 +60,9 @@ export class Invoice {
@Column({ name: 'paid_at', type: 'timestamptz', nullable: true })
paidAt?: Date;
@OneToMany(() => InvoiceItem, (item) => item.invoiceId, { eager: false })
items?: InvoiceItem[];
@CreateDateColumn({ type: 'timestamptz' })
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
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';
@Entity({ name: 'payment_methods', schema: 'public' })
@Entity({ name: 'billing_payment_methods', schema: 'public' })
export class PaymentMethod {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ name: 'tenant_id', type: 'varchar', length: 100 })
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId!: string;
@Column({ type: 'varchar', length: 20 })
@ -14,24 +14,21 @@ export class PaymentMethod {
@Column({ name: 'display_name', type: 'varchar', length: 200, default: '' })
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 })
isDefault!: boolean;
@Column({ type: 'jsonb', default: '{}' })
details!: Record<string, any>; // { last4: '4242', brand: 'visa' } — NEVER store full card numbers
@Column({ type: 'jsonb', nullable: true })
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;
@Column({ name: 'expires_at', type: 'timestamptz', nullable: true })
expiresAt?: Date;
@CreateDateColumn({ type: 'timestamptz' })
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt!: Date;
@UpdateDateColumn({ type: 'timestamptz' })
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt!: Date;
}

View File

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

View File

@ -1,55 +1,49 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity({ name: 'plans', schema: 'public' })
@Entity({ name: 'billing_plans', schema: 'public' })
export class Plan {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ type: 'varchar', length: 30, unique: true })
@Column({ type: 'varchar', length: 50, unique: true })
name!: string; // 'free' | 'pro' | 'enterprise'
@Column({ type: 'varchar', length: 100 })
@Column({ name: 'display_name', type: 'varchar', length: 100 })
displayName!: string;
@Column({ type: 'int', default: 0 })
@Column({ name: 'monthly_price_usd_cents', type: 'int', default: 0 })
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
@Column({ type: 'bigint', default: 100000 })
@Column({ name: 'included_tokens_per_month', type: 'bigint', default: 100000 })
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
@Column({ type: 'int', default: 5 })
@Column({ name: 'max_servers', type: 'int', default: 5 })
maxServers!: number; // -1 = unlimited
@Column({ type: 'int', default: 3 })
@Column({ name: 'max_users', type: 'int', default: 3 })
maxUsers!: number; // -1 = unlimited
@Column({ type: 'int', default: 10 })
@Column({ name: 'max_standing_orders', type: 'int', default: 10 })
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
@Column({ type: 'jsonb', default: '{}' })
features!: Record<string, boolean>;
@Column({ type: 'boolean', default: true })
isActive!: boolean;
@Column({ type: 'int', default: 0 })
sortOrder!: number;
@Column({ type: 'int', default: 14 })
@Column({ name: 'trial_days', type: 'int', default: 0 })
trialDays!: number;
@CreateDateColumn({ type: 'timestamptz' })
@Column({ name: 'is_active', type: 'boolean', default: true })
isActive!: boolean;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt!: Date;
@UpdateDateColumn({ type: 'timestamptz' })
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt!: Date;
}

View File

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

View File

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

View File

@ -62,14 +62,7 @@ export class InvoiceController {
periodEnd: invoice.periodEnd,
dueDate: invoice.dueDate,
paidAt: invoice.paidAt,
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,
})),
items: [],
};
}