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
- Never update transactions – Transactions are append-only
- Never delete transactions – Keep complete history
- Corrections via new transactions – Create reversing transaction if needed
Transaction Consistency
- Created in same transaction – Use database transactions
- Reference integrity – reference field must link to valid entity
- Amount validation – Must be positive
- Currency consistency – Match related entities
Status Management
- Pending → Completed – Normal flow
- Pending → Failed – If operation fails
- 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_receivedtransaction (customer) - Order completion →
payment_releasedtransaction (seller) +commission_earnedtransaction (platform) - Order refund →
payment_refundedtransaction (customer)
With Payments Domain
- Payment verified →
payment_receivedtransaction - Payment released →
payment_releasedtransaction - Payment refunded →
payment_refundedtransaction
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 →
withdrawaltransaction - 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.