Common Base Project

    13. Payment infrastructure

    Members only · Non-members can read 30% of the article.

    Published
    August 10, 2025
    Reading Time
    9 min read
    Author
    Felix
    Access
    Members only
    Preview only

    Non-members can read 30% of the article.

    The payment infrastructure is divided into three chapters. The first chapter is about the payment infrastructure of the core business, the second chapter is about the payment access of the three parties, and the third chapter is the backend + user payment integration.

    This is also divided into two layers. After the bottom table structure and core API are completed, the upper layer only expands third-party payment methods and is compatible with the current payment design and business part.

    Tag of this chapter

    There are four core tables related to payment: product table, order table, subscription table, and transaction history table.

    1. Product list (products)

    The products table stores information about all products or subscriptions that can be purchased.

    export type ProductType = 'one_time' | 'subscription'
    export type Currency = 'USD' | 'CNY' | 'EUR' | 'JPY' | 'GBP'
    export type SubscriptionInterval = 'day' | 'week' | 'month' | 'year'
    
    export const products = sqliteTable('products', {
      id: text('id')
        .primaryKey()
        .$defaultFn(() => crypto.randomUUID()),
      name: text('name').notNull(),
      description: text('description'),
      type: text('type').$type<ProductType>().notNull(),
      price: real('price').notNull(),
      currency: text('currency').$type<Currency>().notNull().default('USD'),
      interval: text('interval').$type<SubscriptionInterval>(), // only for subscription
      tokenAmount: integer('tokenAmount'), // If the product provides tokens
      active: integer('active', { mode: 'boolean' }).notNull().default(true),
      createdAt: integer('created_at', { mode: 'timestamp_ms' })
        .default(sql`CURRENT_TIMESTAMP`)
        .notNull(),
      updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
        .default(sql`CURRENT_TIMESTAMP`)
        .notNull()
    })
    ```
    
    **Field explanation:**
    
    1. **name**: product name, required field
    2. **description**: product description
    3. **type**: product type, using the `ProductType` enumeration type, which can be a one-time purchase (`one_time`) or subscription (`subscription`)
    4. **price**: Product price, stored as a floating point number
    5. **currency**: Currency type, using the `Currency` enumeration type, the default is USD
    6. **interval**: Subscription period, only valid for subscription type products, can be day (`day`), week (`week`), month (`month`) or year (`year`)
    7. **tokenAmount**: The number of tokens provided by the product, used for products that provide tokens
    8. **active**: Whether the product is activated, Boolean value, default is true
    9. **createdAt**: product creation time, automatically set to the current timestamp
    10. **updatedAt**: product update time, automatically set to the current timestamp
    
    ### 2. Orders table (orders)
    
    The order table records all transaction information, including one-time purchases and initial transactions for subscriptions.
    
    ```typescript
    export type OrderStatus = 'pending' | 'completed' | 'failed' | 'refunded'
    
    export type PaymentMethod = 'credit_card' | 'paypal' | 'upgrade.chat' | 'other'
    
    export const orders = sqliteTable('orders', {
      id: text('id')
        .primaryKey()
        .$defaultFn(() => crypto.randomUUID()),
      userId: text('userId')
        .notNull()
        .references(() => users.id),
      productId: text('productId')
        .notNull()
        .references(() => products.id),
      amount: real('amount').notNull(),
      currency: text('currency').$type<Currency>().notNull(),
      status: text('status').$type<OrderStatus>().notNull().default('pending'),
      paymentMethod: text('paymentMethod').$type<PaymentMethod>(),
      paymentIntentId: text('paymentIntentId'), // Transaction ID of payment gateway
      metadata: text('metadata'), // Store additional information in JSON format
      createdAt: integer('created_at', { mode: 'timestamp_ms' })
        .default(sql`CURRENT_TIMESTAMP`)
        .notNull(),
      updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
        .default(sql`CURRENT_TIMESTAMP`)
        .notNull()
    })
    ```
    
    **Field explanation:**
    
    1. **userId**: The ID of the user who placed the order, the foreign key is associated with users
    2. **productId**: purchased product ID, foreign key associated with the products table
    3. **amount**: order amount, stored as a floating point number
    4. **currency**: Currency type, using the `Currency` enumeration type
    5. **status**: This is used to track the complete life cycle of the order, the order status, using the `OrderStatus` enumeration type, the default is `pending`, which can be pending (`pending`), completed (`completed`), failed (`failed`) or refunded (`refunded`)
    6. **paymentMethod**: Payment method, using the `PaymentMethod` enumeration type, which can be credit card (`credit_card`), PayPal (`paypal`), etc.
    7. **paymentIntentId**: Transaction ID of the external payment gateway, used for integration with payment service providers
    8. **metadata**: Additional metadata, stored in JSON format, can be used to store business-specific information
    9. **createdAt**: order creation time, automatically set to the current timestamp
    10. **updatedAt**: order update time, automatically set to the current timestamp
    
    ### 3. Subscriptions
    
    The subscription table records the user's active subscription information and tracks the status and cycle of the subscription.
    
    ```typescript
    export type SubscriptionStatus = 'active' | 'canceled' | 'expired' | 'past_due'
    
    export const subscriptions = sqliteTable('subscriptions', {
      id: text('id')
        .primaryKey()
        .$defaultFn(() => crypto.randomUUID()),
      userId: text('userId')
        .notNull()
        .references(() => users.id, { onDelete: 'cascade' }),
      productId: text('productId')
        .notNull()
        .references(() => products.id),
      orderId: text('orderId').references(() => orders.id),
      status: text('status').$type<SubscriptionStatus>().notNull().default('active'),
      currentPeriodStart: integer('current_period_start', { mode: 'timestamp_ms' }).notNull(),
      currentPeriodEnd: integer('current_period_end', { mode: 'timestamp_ms' }).notNull(),
      cancelAtPeriodEnd: integer('cancel_at_period_end', { mode: 'boolean' }).default(false),
      subscriptionId: text('subscriptionId'), // Subscription ID of payment gateway
      metadata: text('metadata'), // Store additional information in JSON format
      createdAt: integer('created_at', { mode: 'timestamp_ms' })
        .default(sql`CURRENT_TIMESTAMP`)
        .notNull(),
      updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
        .default(sql`CURRENT_TIMESTAMP`)
        .notNull()
    })
    ```
    
    **Field explanation:**
    
    1. **userId**: The ID of the subscriber, and the foreign key is associated with the users table
    2. **productId**: Subscribed product ID, foreign key associated with the products table
    3. **orderId**: Create the initial order ID of this subscription, and associate the foreign key to the orders table
    4. **status**: Subscription status, using the `SubscriptionStatus` enumeration type, the default is `active`, which can be active (`active`), canceled (`canceled`), expired (`expired`) or overdue (`past_due`)
    5. **currentPeriodStart**: The start time of the current subscription period, timestamp format
    6. **currentPeriodEnd**: The end time of the current subscription period, timestamp format
    7. **cancelAtPeriodEnd**: Whether to cancel the subscription at the end of the current period, Boolean value, default is false
    8. **subscriptionId**: Subscription ID of the external payment gateway, used for integration with payment service providers
    9. **metadata**: Additional metadata, stored in JSON format
    10. **createdAt**: Subscription creation time, automatically set to the current timestamp
    11. **updatedAt**: Subscription update time, automatically set to the current timestamp
    
    ### 4. Transaction history table (transactions)
    
    The transaction history table records all changes in token balance, including purchase, use, refund and other operations.
    
    ```typescript
    export const transactions = sqliteTable('transactions', {
      id: text('id')
        .primaryKey()
        .$defaultFn(() => crypto.randomUUID()),
      userId: text('userId')
        .notNull()
        .references(() => users.id, { onDelete: 'cascade' }),
      orderId: text('orderId').references(() => orders.id),
      type: text('type').$type<TransactionType>().notNull(),
      amount: integer('amount').notNull(), //Token quantity, which can be positive (increase) or negative (consumption)
      createdAt: integer('created_at', { mode: 'timestamp_ms' })
        .default(sql`CURRENT_TIMESTAMP`)
        .notNull()
    })
    ```
    
    **Field explanation:**
    
    1. **userId**: The user ID associated with the transaction, the foreign key is associated with the users table, and cascade deletion is set.
    2. **orderId**: associated order ID (if applicable), foreign key associated to the orders table
    3. **type**: transaction type, using the `TransactionType` enumeration type, which can be purchase (`purchase`), use (`usage`), refund (`refund`), subscription renewal (`subscription_renewal`), gift (`gift`) or promotion (`promotion`)
    4. **amount**: token quantity change, integer type, positive number indicates increase, negative number indicates consumption
    5. **createdAt**: Transaction creation time, automatically set to the current timestamp
    
    ### Relationships and data flows between tables
    
    Let’s sort out the data flow and business logic between all tables
    
    #### 1. **User purchases product**:
    
    - Create `orders` table record with status `pending`
    - After the payment is successful, update the `orders` table record status to `completed`
    - Create `transactions` table record, type is `purchase`, amount is positive number
    - Update the `userUsage` table and add `totalTokens`
    
    **Data flow**:
    
    
    Members only

    Subscribe to unlock the full article

    Support the writing, unlock every paragraph, and receive future updates instantly.

    Comments

    Join the conversation

    0 comments
    Sign in to comment

    No comments yet. Be the first to add one.