Skip to content

Transactions Domain

The Transactions domain provides a unified view of all financial transactions across the platform. It aggregates data from orders, payments, accounts, and withdrawals to create a comprehensive transaction history.

Overview

The Transactions domain serves as:

  • Financial history – Complete record of all money movements
  • Reporting layer – Data for financial reports and analytics
  • Audit trail – Compliance and accountability
  • User transparency – Allow users to see their transaction history

Entity

Transaction Entity

// domain/transactions/transaction.entity.ts
export class Transaction {
  id: string;
  userId: string;
  type: TransactionType;
  amount: Decimal;
  currency: string;
  status: TransactionStatus;
  reference: string; // Order ID, Payment ID, Withdraw ID, etc.
  description: string;
  metadata: TransactionMetadata;
  createdAt: Date;

  static create(
    id: string,
    userId: string,
    type: TransactionType,
    amount: Decimal,
    currency: string,
    status: TransactionStatus,
    reference: string,
    description: string,
    metadata: TransactionMetadata
  ): Transaction
}

export type TransactionType =
  | 'payment_received'    // Customer paid for order
  | 'payment_released'    // Seller received payout for completed order
  | 'payment_refunded'    // Customer received refund
  | 'commission_earned'   // Platform earned commission
  | 'withdrawal'          // User withdrew funds
  | 'balance_adjustment'  // Admin adjusted balance

export type TransactionStatus =
  | 'pending'      // Transaction initiated but not completed
  | 'completed'    // Transaction successful
  | 'failed'       // Transaction failed
  | 'cancelled'    // Transaction cancelled

export type TransactionMetadata = {
  orderId?: string;
  paymentId?: string;
  withdrawId?: string;
  serviceId?: string;
  serviceTitle?: string;
  sellerId?: string;
  customerId?: string;
  commissionRate?: number;
  bankAccount?: BankAccountDetails;
  [key: string]: any;
}

Key properties: - userId – The user affected by the transaction (seller, customer, or platform) - type – Category of transaction - amount – Always positive (use type to determine debit/credit) - reference – Links to source entity (Order, Payment, Withdraw) - metadata – Additional context (order details, bank account, etc.)

Repository

TransactionRepository

// adapters/transaction/transaction.repository.ts
export abstract class TransactionRepository extends AbstractRepository<
  Transaction,
  string,
  TransactionQueryPolicy
> {
  abstract getByUserId(
    userId: string,
    pagination: Pagination
  ): Promise<Paginated<Transaction>>;

  abstract getByReference(reference: string): Promise<Transaction[]>;

  abstract getTotalByType(
    userId: string,
    type: TransactionType,
    fromDate?: Date,
    toDate?: Date
  ): Promise<Decimal>;

  abstract getMonthlyReport(
    userId: string,
    year: number,
    month: number
  ): Promise<MonthlyTransactionReport>;
}

export class TransactionQueryPolicy {
  userId?: string;
  type?: TransactionType;
  status?: TransactionStatus;
  currency?: string;
  fromDate?: Date;
  toDate?: Date;
  minAmount?: Decimal;
  maxAmount?: Decimal;
}

export type MonthlyTransactionReport = {
  year: number;
  month: number;
  totalReceived: Decimal;
  totalReleased: Decimal;
  totalRefunded: Decimal;
  totalWithdrawn: Decimal;
  netIncome: Decimal; // received - refunded - withdrawn
  transactionCount: number;
}

Transaction Creation

Transactions are created automatically by other domain events:

From Order Completion

// When order is completed
const sellerTransaction = Transaction.create(
  generateId(),
  order.sellerId,
  'payment_released',
  order.totalAmount.minus(order.commissionAmount),
  'NGN',
  'completed',
  order.id,
  `Payout for order ${order.getNumber()}`,
  {
    orderId: order.id,
    paymentId: order.paymentId,
    serviceId: order.serviceId,
    serviceTitle: order.serviceTitle,
    customerId: order.customerId,
    commissionRate: order.commissionAmount.div(order.totalAmount).toNumber()
  }
);

const platformTransaction = Transaction.create(
  generateId(),
  'platform',
  'commission_earned',
  order.commissionAmount,
  'NGN',
  'completed',
  order.id,
  `Commission from order ${order.getNumber()}`,
  {
    orderId: order.id,
    serviceId: order.serviceId,
    sellerId: order.sellerId,
    customerId: order.customerId
  }
);

From Payment

// When customer pays for order
const transaction = Transaction.create(
  generateId(),
  payment.userId,
  'payment_received',
  payment.amount,
  payment.currency,
  'completed',
  payment.id,
  'Payment for order',
  {
    paymentId: payment.id,
    orderId: order.id,
    serviceId: order.serviceId,
    sellerId: order.sellerId
  }
);

From Refund

// When refund is processed
const transaction = Transaction.create(
  generateId(),
  refund.userId,
  'payment_refunded',
  refund.amount,
  refund.currency,
  'completed',
  refund.id,
  `Refund for order ${order.getNumber()}`,
  {
    orderId: refund.orderId,
    paymentId: refund.paymentId,
    reason: refund.reason
  }
);

From Withdrawal

// When withdrawal is completed
const transaction = Transaction.create(
  generateId(),
  withdraw.userId,
  'withdrawal',
  withdraw.amount,
  withdraw.currency,
  'completed',
  withdraw.id,
  'Withdrawal to bank account',
  {
    withdrawId: withdraw.id,
    bankAccount: withdraw.bankAccount
  }
);

From Balance Adjustment

// When admin adjusts balance
const transaction = Transaction.create(
  generateId(),
  userId,
  'balance_adjustment',
  amount,
  currency,
  'completed',
  `adjustment-${Date.now()}`,
  reason,
  {
    adjustedBy: adminId,
    adjustmentReason: reason
  }
);

Business Rules

Transaction Immutability

  1. Never update transactions – Transactions are append-only
  2. Never delete transactions – Keep complete history
  3. Corrections via new transactions – Create reversing transaction if needed

Transaction Consistency

  1. Created in same transaction – Use database transactions
  2. Reference integrity – reference field must link to valid entity
  3. Amount validation – Must be positive
  4. Currency consistency – Match related entities

Status Management

  1. Pending → Completed – Normal flow
  2. Pending → Failed – If operation fails
  3. No reversal of completed – Use compensating transaction instead

Use Cases

User Transaction History

Get all transactions for a user:

// app/transactions/transaction.usecases.ts
async getUserTransactions(
  userId: string,
  pagination: Pagination
): Promise<Paginated<Transaction>> {
  return this.transactionRepository.getByUserId(userId, pagination);
}

Filter Transactions

async filterTransactions(
  userId: string,
  filters: {
    type?: TransactionType;
    status?: TransactionStatus;
    fromDate?: Date;
    toDate?: Date;
  },
  pagination: Pagination
): Promise<Paginated<Transaction>> {
  const policy = new TransactionQueryPolicy();
  policy.userId = userId;
  policy.type = filters.type;
  policy.status = filters.status;
  policy.fromDate = filters.fromDate;
  policy.toDate = filters.toDate;

  return this.transactionRepository.findAll(policy, pagination);
}

Calculate Earnings

async calculateEarnings(
  userId: string,
  fromDate?: Date,
  toDate?: Date
): Promise<{
  received: Decimal;
  released: Decimal;
  refunded: Decimal;
  withdrawn: Decimal;
  available: Decimal;
}> {
  const received = await this.transactionRepository.getTotalByType(
    userId,
    'payment_received',
    fromDate,
    toDate
  );

  const released = await this.transactionRepository.getTotalByType(
    userId,
    'payment_released',
    fromDate,
    toDate
  );

  const refunded = await this.transactionRepository.getTotalByType(
    userId,
    'payment_refunded',
    fromDate,
    toDate
  );

  const withdrawn = await this.transactionRepository.getTotalByType(
    userId,
    'withdrawal',
    fromDate,
    toDate
  );

  // Net earnings = released - withdrawn
  const available = released.minus(withdrawn);

  return { received, released, refunded, withdrawn, available };
}

Monthly Report

async getMonthlyReport(
  userId: string,
  year: number,
  month: number
): Promise<MonthlyTransactionReport> {
  return this.transactionRepository.getMonthlyReport(userId, year, month);
}

Export Transactions

async exportTransactions(
  userId: string,
  format: 'csv' | 'pdf',
  filters?: TransactionQueryPolicy
): Promise<Buffer> {
  const transactions = await this.transactionRepository.findAll(
    filters || { userId },
    { page: 1, limit: 10000 }
  );

  if (format === 'csv') {
    return this.generateCSV(transactions.data);
  } else {
    return this.generatePDF(transactions.data);
  }
}

Integration Points

With Orders Domain

  • Order creation → payment_received transaction (customer)
  • Order completion → payment_released transaction (seller) + commission_earned transaction (platform)
  • Order refund → payment_refunded transaction (customer)

With Payments Domain

  • Payment verified → payment_received transaction
  • Payment released → payment_released transaction
  • Payment refunded → payment_refunded transaction

With Accounts Domain

  • Account credited → Transaction created with appropriate type
  • Account debited → Transaction created (withdrawal, refund)
  • AccountEvent mirrors Transaction (different granularity)

With Withdrawals

  • Withdrawal completed → withdrawal transaction
  • Withdrawal failed → Transaction status = 'failed'

Key Workflows

Order Payment Flow (Customer Perspective)

1. Customer pays for order
2. Payment verified
3. Create Transaction:
   - type: 'payment_received'
   - amount: payment.amount
   - status: 'completed'
   - userId: customer.id
4. Order created

Order Completion Flow (Seller Perspective)

1. Order marked as completed
2. Calculate payout and commission
3. Create Transaction for seller:
   - type: 'payment_released'
   - amount: totalAmount - commission
   - status: 'completed'
   - userId: seller.id
4. Create Transaction for platform:
   - type: 'commission_earned'
   - amount: commission
   - status: 'completed'
   - userId: 'platform'
5. Seller can view in transaction history

Withdrawal Flow

1. Seller requests withdrawal
2. Admin approves
3. Transfer initiated
4. Transfer completed
5. Create Transaction:
   - type: 'withdrawal'
   - amount: withdrawal.amount
   - status: 'completed'
   - userId: seller.id
6. Seller sees withdrawal in history

Analytics and Reporting

Platform Analytics

// Total revenue (commission)
const totalRevenue = await transactionRepository.getTotalByType(
  'platform',
  'commission_earned',
  startDate,
  endDate
);

// Total payouts to sellers
const totalPayouts = await transactionRepository.getTotalByType(
  'platform',
  'payment_released',
  startDate,
  endDate
);

// Total refunds issued
const totalRefunds = await transactionRepository.getTotalByType(
  'platform',
  'payment_refunded',
  startDate,
  endDate
);

Seller Analytics

// Total earnings
const earnings = await transactionRepository.getTotalByType(
  sellerId,
  'payment_released',
  startDate,
  endDate
);

// Total withdrawn
const withdrawn = await transactionRepository.getTotalByType(
  sellerId,
  'withdrawal',
  startDate,
  endDate
);

// Available balance = earnings - withdrawn
const available = earnings.minus(withdrawn);

Customer Analytics

// Total spent
const spent = await transactionRepository.getTotalByType(
  customerId,
  'payment_received',
  startDate,
  endDate
);

// Total refunded
const refunded = await transactionRepository.getTotalByType(
  customerId,
  'payment_refunded',
  startDate,
  endDate
);

// Net spending = spent - refunded
const netSpending = spent.minus(refunded);

Data Model

┌──────────────────┐
│   Transaction    │
├──────────────────┤
│ id               │
│ userId           │────► User
│ type             │
│ amount           │
│ currency         │
│ status           │
│ reference        │────► Order/Payment/Withdraw
│ description      │
│ metadata         │
│ createdAt        │
└──────────────────┘

Relationships: - Many-to-one with User - Reference field links to Order, Payment, or Withdraw - Metadata stores related entity IDs

Transaction Display

Format for Users

function formatTransaction(transaction: Transaction): string {
  const sign = ['payment_released', 'payment_refunded'].includes(transaction.type)
    ? '+'
    : '-';

  const amount = `${sign}${transaction.currency} ${formatMoney(transaction.amount)}`;

  return {
    id: transaction.id,
    date: formatDate(transaction.createdAt),
    description: transaction.description,
    amount,
    status: transaction.status,
    type: formatType(transaction.type)
  };
}

function formatType(type: TransactionType): string {
  const typeLabels = {
    payment_received: 'Payment',
    payment_released: 'Payout',
    payment_refunded: 'Refund',
    commission_earned: 'Commission',
    withdrawal: 'Withdrawal',
    balance_adjustment: 'Adjustment'
  };

  return typeLabels[type];
}

Example Display

Date              | Description                    | Amount        | Status
------------------|--------------------------------|---------------|----------
Jan 15, 2024      | Payout for order #1234        | +NGN 25,000   | Completed
Jan 12, 2024      | Withdrawal to bank account     | -NGN 10,000   | Completed
Jan 10, 2024      | Payout for order #1230        | +NGN 15,000   | Completed
Jan 08, 2024      | Payment for order #1235       | -NGN 30,000   | Completed
Jan 05, 2024      | Refund for order #1228        | +NGN 20,000   | Completed

Best Practices

✅ Do

  • Create transaction for every money movement – Complete audit trail
  • Include detailed metadata – Store context for future reference
  • Use consistent descriptions – Standardized format for readability
  • Preserve history – Never delete transactions
  • Link to source entities – Use reference field properly
  • Generate reports – Provide users with transaction summaries
  • Export capability – Allow CSV/PDF export for users

❌ Don't

  • Don't update transactions – They are immutable
  • Don't delete transactions – Keep complete history
  • Don't use negative amounts – Use type to indicate direction
  • Don't skip metadata – Always include relevant context
  • Don't create duplicate transactions – Check reference before creating
  • Don't expose platform transactions to users – Filter by userId
  • Don't hard-code descriptions – Use templates with variables

Validation Rules

Transaction Creation

// app/transactions/transaction.validators.ts
export const transactionSchema = z.object({
  userId: z.string().uuid(),
  type: z.enum([
    'payment_received',
    'payment_released',
    'payment_refunded',
    'commission_earned',
    'withdrawal',
    'balance_adjustment'
  ]),
  amount: z.number().positive('Amount must be positive'),
  currency: z.enum(['NGN', 'USD']),
  status: z.enum(['pending', 'completed', 'failed', 'cancelled']),
  reference: z.string().min(1),
  description: z.string().min(10, 'Description too short'),
  metadata: z.record(z.any()).optional()
});

Summary

The Transactions domain provides:

  • Unified transaction history across all domains
  • Financial reporting for users and platform
  • Audit trail for compliance and accountability
  • Analytics for earnings, spending, and revenue
  • Transparency for users to track money movements
  • Export capability for external record-keeping
  • Immutable records for data integrity

This domain aggregates financial data from Orders, Payments, Accounts, and Withdrawals to provide a complete view of platform financial activity.