Building a Transaction System with MongoDB
Transaction systems are at the heart of modern commerce and financial activities, spanning a wide range of scenarios from e-commerce order processing to real-time settlements in financial institutions. Such systems demand high concurrency handling, real-time data storage, and efficient retrieval capabilities, all while balancing data consistency and performance. As transaction data continues to grow in scale and complexity, traditional transaction system architectures often struggle to meet these challenges due to rigid table structures and limited horizontal scalability.
MongoDB, as a distributed document-oriented database, provides a modern solution for building complex and efficient transaction systems. With its flexible schema design, high throughput capabilities, and built-in transaction support, MongoDB addresses diverse business needs effectively.
Problem Statement and Challenges
The core functionalities of a transaction system include account management, balance inquiry, recharge, refund, and managing and querying transaction records. For instance, the following illustrates how a user's account balance is displayed:
Displays the user's transaction history:
Building an efficient and reliable modern transaction system involves tackling several key challenges:
High Concurrency
Transaction systems must handle large-scale concurrent requests, particularly in domains like finance and e-commerce, where users frequently perform transactions, balance inquiries, and other operations simultaneously. Failure to support high concurrency can result in delayed responses or even system crashes.Data Consistency and Transaction Management
Ensuring data consistency is critical, especially for sensitive operations like account balance updates, transaction records, and refunds. Any inconsistency could lead to financial risks and a loss of user trust. Transaction management in distributed environments adds another layer of complexity.Flexible Data Modeling
Traditional relational databases often rely on rigid table structures that struggle to adapt to the complex and evolving business logic of transaction systems. Adding new features often requires significant changes to the data model, leading to high modification costs and potential service disruptions.Real-Time Data Processing and Query Performance
With massive transaction volumes, the system must efficiently store, retrieve, and process real-time user queries, such as balance checks and transaction history retrievals. This demands a database with high throughput and lightning-fast query performance.System Scalability
As the business grows, the volume of transaction data will continue to expand. The system must support seamless horizontal scaling, ensuring that performance improves linearly as additional servers or storage nodes are added.
Data Modeling
To achieve large-scale data storage and high-concurrency read/write capabilities, data modeling is the key challenge. MongoDB's document model is renowned for its flexibility and efficiency, allowing it to easily accommodate the complex and evolving needs of transaction systems.
The following six APIs define the essential functionalities of the transaction system:
api/CreateAccount
Creates a user account, initializing basic user information and the initial balance.api/GetBalance
Retrieves the account balance, ensuring real-time accuracy and consistency.api/GetTransactionById
Fetches transaction details based on a transaction ID, supporting quick and precise lookups.api/GetTransactions
Retrieves historical transaction records for a specific account, with support for pagination and time-range filtering to handle large-scale data queries.api/Recharge
Processes account recharges, involving transaction handling to ensure synchronized updates to the account balance and transaction records.api/Refund
Handles refund operations, ensuring transaction integrity and detailed operation records.
To address the above requirements, two collections are designed to
store user account states and detailed transaction information. The two
collections are linked through Uuid
, enabling efficient
queries and updates.
AccountEntry
represents the basic account information for each user in the system, with the following key fields:
Uuid
(string)
A unique identifier for each account, used to uniquely locate an account within the system. Globally unique.Balance
(decimal)
The current balance of the account, stored as a precise decimal value to record the total amount of funds available to the user.
The AccountEntry
definition:
public class AccountEntry
{
public string Uuid { get; set; }
public decimal Balance { get; set; }
public DateTime CreateTime { get; set; }
public DateTime UpdateTime { get; set; }
}
TransEntry
records each transaction in the system and supports various types of operations. The field descriptions are as follows:
Tid
(string)
A unique identifier for each transaction, used to uniquely locate a specific transaction.Uuid
(string)
The unique identifier of the corresponding account, serving as a foreign key linked toAccountEntry
, indicating which account this transaction belongs to.Amount
(decimal)
The transaction amount, recording the funds involved in this transaction.NewBalance
(decimal)
A snapshot of the account balance after the transaction is completed, enabling the tracking of fund movements and supporting auditing functionality.Type
(TransType)
An enumeration of transaction types, indicating the nature of the transaction.
The TransEntry
definition:
public class TransEntry
{
public string Tid { get; set; }
public string Uuid { get; set; }
public decimal Amount { get; set; }
public decimal NewBalance { get; set; }
public TransType Type { get; set; }
public DateTime CreateTime { get; set; }
public DateTime UpdateTime { get; set; }
}
public enum TransType
{
None = 0,
Recharge = 1,
Refund = 2,
}
System Implementation
Here we implement each API and determine the required indexes and shard keys based on actual read and write patterns. The APIs are introduced step-by-step, starting with simpler ones and progressing to more complex implementations.
CreateAccount API
The CreateAccount
API is straightforward. It does not
use transaction processing because the user's Uuid
serves
as the primary key. If an account with the same Uuid
already exists, the insert operation will fail due to the unique
constraint.
public async Task<AccountEntry> CreateAccountAsync(string uuid, CancellationToken cancellationToken = default)
{
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
{
var newAccount = new AccountEntry
{
Uuid = uuid,
Balance = 0,
CreateTime = DateTime.UtcNow,
UpdateTime = DateTime.UtcNow
};
await _accountCollection.InsertOneAsync(newAccount, cancellationToken: cts.Token);
return newAccount;
}
}
GetBalance API
The implementation of retrieving the balance is straightforward. Note
that the IClientSessionHandle
parameter is optional. When
this parameter is provided, it serves as a basic building block of
complex transactions:
public async Task<decimal> GetBalanceAsync(string uuid, CancellationToken ct, IClientSessionHandle s = null)
{
AccountEntry account;
if (s == null)
{
account = await _accountCollection.Find(x => x.Uuid == uuid).FirstOrDefaultAsync(ct);
}
else
{
account = await _accountCollection.Find(s, x => x.Uuid == uuid).FirstOrDefaultAsync(ct);
}
if (account == null)
{
throw new KeyNotFoundException($"Account '{uuid}' does not exist.");
}
return account.Balance;
}
This API requires Account
index:(Uuid).
AddBalance & DecreaseBalance API
The operations on the balance are also the foundation for
constructing subsequent transaction APIs. The implementation of these
two APIs uses MongoDB's Inc
operator for atomic operations,
and they also support transactions. They will not be used independently,
as both increasing or decreasing the balance require corresponding
transaction records. Therefore, the IClientSessionHandle
parameter is mandatory:
private async Task<AccountEntry> AddBalanceAsync(string accountId, decimal amount, CancellationToken ct, IClientSessionHandle s)
{
if (amount <= 0)
{
throw new ArgumentException("Amount should > 0");
}
var filter = Builders<AccountEntry>.Filter.Eq(x => x.Uuid, accountId);
var update = Builders<AccountEntry>.Update
.Inc(x => x.Balance, amount)
.Set(x => x.UpdateTime, DateTime.UtcNow);
var options = new FindOneAndUpdateOptions<AccountEntry> { ReturnDocument = ReturnDocument.After, IsUpsert = true };
return await _accountCollection.FindOneAndUpdateAsync(s, filter, update, options, cancellationToken: ct);
}
private async Task<AccountEntry> DecreaseBalanceAsync(string accountId, decimal amount, CancellationToken ct, IClientSessionHandle s)
{
if (amount <= 0)
{
throw new ArgumentException("Amount should > 0");
}
var filter = Builders<AccountEntry>.Filter.And(
Builders<AccountEntry>.Filter.Eq(x => x.Uuid, accountId),
Builders<AccountEntry>.Filter.Gte(x => x.Balance, amount)
);
var update = Builders<AccountEntry>.Update
.Inc(x => x.Balance, -amount)
.Set(x => x.UpdateTime, DateTime.UtcNow);
var options = new FindOneAndUpdateOptions<AccountEntry> { ReturnDocument = ReturnDocument.After };
return await _accountCollection.FindOneAndUpdateAsync(s, filter, update, options, cancellationToken: ct);
}
It is worth mentioning that in our design, the amount in both of
these APIs is always a positive number. The advantage of this is that
when decreasing the balance, we can include
Filter.Gte(x => x.Balance, amount)
, which further
ensures the correctness of the balance deduction.
These two APIs require an index on the Account
table:
(Uuid)
.
GetTransactionByTid & GetTransactions API
Both APIs are simple:
public async Task<TransEntry> GetTransactionByTidAsync(string tid, CancellationToken ct, IClientSessionHandle s = null)
{
if (s == null)
{
return await _transCollection.Find(x => x.Tid == tid).FirstOrDefaultAsync(ct);
}
return await _transCollection.Find(s, x => x.Tid == tid).FirstOrDefaultAsync(ct);
}
public async Task<List<TransEntry>> GetTransactionsAsync(string accountId, CancellationToken ct, int n = 10)
{
var result = await _transCollection.Find(x => x.Uuid == accountId).SortBy(x => x.CreateTime).Limit(n).ToListAsync(ct);
return result;
}
These two APIs require an index on the Trans
table:
(Uuid, CreateTime), (Tid).
Recharge API
Here come the two most complex transaction APIs:
Recharge
and Refund
. Note that these two APIs
not only ensure consistency, but also need to guarantee idempotency,
meaning they must support repeated calls without affecting the final
result.
For Recharge
, three operations are involved, which
require a transaction:
GetTransactionByTid
: The caller needs to pass in theTid
to prevent duplicate transactions. Idempotency is ensured by this step. In practical applications, theTid
can be the unique identifier returned by a third-party financial system.AddBalance
: This API was implemented earlier.InsertTransaction
: Adds a recharge transaction record.
If any of the above three operations fail, MongoDB's transaction can be used to roll back the changes, ensuring strong consistency of the transaction:
public async Task<TransResult> RechargeAsync(string accountId, string tid, decimal amount)
{
using (var session = await _mongoClient.StartSessionAsync())
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
{
return await session.WithTransactionAsync(
async (s, ct) =>
{
var existingTransEntry = await GetTransactionByTidAsync(tid, ct, s);
if (existingTransEntry != null)
{
return new TransResult { Code = TransCode.DuplicateTrans };
}
var newBalanceEntry = await AddBalanceAsync(accountId, amount, ct, s);
var newTransEntry = new TransEntry
{
Tid = GenerateTid(),
Uuid = accountId,
Amount = amount,
NewBalance = newBalanceEntry.Balance,
Type = TransType.Recharge,
CreateTime = DateTime.UtcNow,
UpdateTime = DateTime.UtcNow,
};
await _transCollection.InsertOneAsync(s, newTransEntry, cancellationToken: ct);
return new TransResult
{
NewBalance = newTransEntry.NewBalance,
Tid = newTransEntry.Tid
};
}, cancellationToken: cts.Token);
}
}
Refund API
The implementation of the Refund
API is essentially the
same as Recharge
, with AddBalance
replaced by
DecreaseBalance
.
public async Task<TransResult> RefundAsync(string accountId, string tid, decimal amount)
{
using (var session = await _mongoClient.StartSessionAsync())
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
{
return await session.WithTransactionAsync(
async (s, ct) =>
{
var existingTransEntry = await GetTransactionByTidAsync(tid, ct, s);
if (existingTransEntry != null)
{
return new TransResult { Code = TransCode.DuplicateTrans };
}
var newBalanceEntry = await DecreaseBalanceAsync(accountId, amount, ct, s);
if (newBalanceEntry == null)
{
return new TransResult { Code = TransCode.InsufficientBalance };
}
var newTransEntry = new TransEntry
{
Tid = GenerateTid(),
Uuid = accountId,
Amount = amount,
NewBalance = newBalanceEntry.Balance,
Type = TransType.Refund,
CreateTime = DateTime.UtcNow,
UpdateTime = DateTime.UtcNow,
};
await _transCollection.InsertOneAsync(s, newTransEntry, cancellationToken: ct);
return new TransResult
{
NewBalance = newTransEntry.NewBalance,
Tid = newTransEntry.Tid
};
},
cancellationToken: cts.Token);
}
}
In practice, the Refund
API can be modified to
incorperate business logic, ensuring consistency between the business
logic and the transaction system.
Indexes and Shard Keys
Based on the above implementations and considering the leftmost
prefix matching principle, we need one indexe on
Account
:
- (Uuid)
Two indexes on Trans
:
- (Uuid, CreateTime)
- (Tid)
Both Account
and Trans
could use
Uuid
as the sharding key.
Test Code
Create 10 accounts and perform an initial recharge of 1000 for each account. Generate 5 transactions for each account, randomly deciding the transaction type (recharge or refund), with transaction amounts between 0 and 300. Print the current balance of each account, as well as the details of the last 10 transactions.
public async Task RunAsync()
{
var random = new Random(42);
// Create 10 accounts
var accountIds = new List<string>();
for (int i = 0; i < 10; i++)
{
var accountId = GenerateAccountUuid();
var account = await CreateAccountAsync(accountId);
accountIds.Add(account.Uuid);
await RechargeAsync(account.Uuid, GenerateTid(), 1000);
Console.WriteLine($"Created account {account.Uuid} with initial balance of 1000.");
}
Console.WriteLine("------------------------------------------------------------------------------");
foreach (var accountId in accountIds)
{
for (int i = 0; i < 5; i++)
{
var isRecharge = random.Next(0, 2) == 0;
var amount = (decimal)(random.NextDouble() * 300);
var tid = GenerateTid();
if (isRecharge)
{
var result = await RechargeAsync(accountId, tid, amount);
if (result.Code == TransCode.Success)
{
Console.WriteLine($"Recharge: Account {accountId}, Amount: {Math.Round(amount, 2)}, New Balance: {Math.Round(result.NewBalance, 2)}, Tid: {tid}");
}
else
{
Console.WriteLine($"Recharge failed: {result.Code}");
}
}
else
{
var result = await RefundAsync(accountId, tid, amount);
if (result.Code == TransCode.Success)
{
Console.WriteLine($"Refund: Account {accountId}, Amount: {Math.Round(amount, 2)}, New Balance: {Math.Round(result.NewBalance, 2)}, Tid: {tid}");
}
else
{
Console.WriteLine($"Refund failed: {result.Code}");
}
}
}
}
Console.WriteLine("------------------------------------------------------------------------------");
foreach (var accountId in accountIds)
{
var balance = await GetBalanceAsync(accountId, CancellationToken.None);
var transactions = await GetTransactionsAsync(accountId, CancellationToken.None);
Console.WriteLine($"Account {accountId} Balance: {Math.Round(balance, 2)}");
Console.WriteLine("Recent Transactions:");
foreach (var transaction in transactions)
{
Console.WriteLine($"Tid: {transaction.Tid}, Type: {transaction.Type}, Amount: {Math.Round(transaction.Amount, 2)}, New Balance: {Math.Round(transaction.NewBalance, 2)}, Time: {transaction.CreateTime}");
}
Console.WriteLine("****************************************************************************");
}
}
Some results:
Account Acc_BE2E2B2FD3A2490CBEEA9625C3BD7C18 Balance: 1267.83 Recent Transactions: Tid: T_6DA267BECAC945E2973903C7EE36C599, Type: Recharge, Amount: 1000, New Balance: 1000, Time: 11/24/2024 4:00:42 AM Tid: T_6BB3C3B2E9CD4197B609957C2937D218, Type: Refund, Amount: 42.27, New Balance: 957.73, Time: 11/24/2024 4:00:43 AM Tid: T_0D63FB52BCBF4FF382D7C611252329F4, Type: Recharge, Amount: 156.83, New Balance: 1114.56, Time: 11/24/2024 4:00:43 AM Tid: T_30201F58B3AA4BD6B14C4B9DC0A20272, Type: Recharge, Amount: 78.78, New Balance: 1193.33, Time: 11/24/2024 4:00:43 AM Tid: T_42837124561748BFBECA1677FD951338, Type: Refund, Amount: 153.88, New Balance: 1039.46, Time: 11/24/2024 4:00:43 AM Tid: T_16DC47F772814D4A8A3E1DBCF26B661D, Type: Recharge, Amount: 228.38, New Balance: 1267.83, Time: 11/24/2024 4:00:43 AM
Connect to the database deployed on MongoDB Atlas using MongoDB Compass:
In this post, we provide complete account management and transaction support functions based on MongoDB, making it suitable for small to medium-sized transaction systems. It is also easily extendable to more complex application scenarios, such as multi-currency support and international payments. Each functional module works together through transactions, filters, and idempotency handling to build a secure, efficient, and scalable transaction system framework.
This design meets the practical business needs of high concurrency, data consistency, and complex transactions, while also providing clear error handling pathways for easier maintenance and expansion.
Full Demo
Fill the ConnectionString
:
using MongoDB.Bson.Serialization.Conventions;
using MongoDB.Driver;
namespace MongoTest
{
class Program
{
static void Main(string[] args)
{
var model = new TransModel();
model.RunAsync().Wait();
Console.WriteLine("Done");
}
}
public class AccountEntry
{
public string Uuid { get; set; }
public decimal Balance { get; set; }
public DateTime CreateTime { get; set; }
public DateTime UpdateTime { get; set; }
}
public class TransEntry
{
public string Tid { get; set; }
public string Uuid { get; set; }
public decimal Amount { get; set; }
public decimal NewBalance { get; set; }
public TransType Type { get; set; }
public DateTime CreateTime { get; set; }
public DateTime UpdateTime { get; set; }
}
public enum TransType
{
None = 0,
Recharge = 1,
Refund = 2,
}
public class TransResult
{
public string Tid { get; set; }
public decimal NewBalance { get; set; }
public TransCode Code { get; set; }
}
public enum TransCode
{
Success = 0,
DuplicateTrans = 1,
Refund = 2,
InsufficientBalance = 3
}
public class TransModel
{
private readonly IMongoClient _mongoClient;
private readonly IMongoCollection<AccountEntry> _accountCollection;
private readonly IMongoCollection<TransEntry> _transCollection;
private const string DatabaseName = "Transaction";
private const string AccountCollectionName = "Account";
private const string TransCollectionName = "Trans";
public const string ConnectionString = "";
public TransModel()
{
_mongoClient = new MongoClient(ConnectionString);
var database = _mongoClient.GetDatabase(DatabaseName);
var ignoreExtraElementsConvention = new ConventionPack { new IgnoreExtraElementsConvention(true) };
ConventionRegistry.Register("IgnoreExtraElements", ignoreExtraElementsConvention, type => true);
_accountCollection = database.GetCollection<AccountEntry>(AccountCollectionName);
_transCollection = database.GetCollection<TransEntry>(TransCollectionName);
}
public static string GenerateTid() => $"T_{Guid.NewGuid().ToString("N").ToUpper()}";
public static string GenerateAccountUuid() => $"Acc_{Guid.NewGuid().ToString("N").ToUpper()}";
public async Task<AccountEntry> CreateAccountAsync(string uuid, CancellationToken cancellationToken = default)
{
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
{
var newAccount = new AccountEntry
{
Uuid = uuid,
Balance = 0,
CreateTime = DateTime.UtcNow,
UpdateTime = DateTime.UtcNow
};
await _accountCollection.InsertOneAsync(newAccount, cancellationToken: cts.Token);
return newAccount;
}
}
public async Task<decimal> GetBalanceAsync(string uuid, CancellationToken ct, IClientSessionHandle s = null)
{
AccountEntry account;
if (s == null)
{
account = await _accountCollection.Find(x => x.Uuid == uuid).FirstOrDefaultAsync(ct);
}
else
{
account = await _accountCollection.Find(s, x => x.Uuid == uuid).FirstOrDefaultAsync(ct);
}
if (account == null)
{
throw new KeyNotFoundException($"Account '{uuid}' does not exist.");
}
return account.Balance;
}
private async Task<AccountEntry> AddBalanceAsync(string accountId, decimal amount, CancellationToken ct, IClientSessionHandle s)
{
if (amount <= 0)
{
throw new ArgumentException("Amount should > 0");
}
var filter = Builders<AccountEntry>.Filter.Eq(x => x.Uuid, accountId);
var update = Builders<AccountEntry>.Update
.Inc(x => x.Balance, amount)
.Set(x => x.UpdateTime, DateTime.UtcNow);
var options = new FindOneAndUpdateOptions<AccountEntry> { ReturnDocument = ReturnDocument.After, IsUpsert = true };
return await _accountCollection.FindOneAndUpdateAsync(s, filter, update, options, cancellationToken: ct);
}
private async Task<AccountEntry> DecreaseBalanceAsync(string accountId, decimal amount, CancellationToken ct, IClientSessionHandle s)
{
if (amount <= 0)
{
throw new ArgumentException("Amount should > 0");
}
var filter = Builders<AccountEntry>.Filter.And(
Builders<AccountEntry>.Filter.Eq(x => x.Uuid, accountId),
Builders<AccountEntry>.Filter.Gte(x => x.Balance, amount)
);
var update = Builders<AccountEntry>.Update
.Inc(x => x.Balance, -amount)
.Set(x => x.UpdateTime, DateTime.UtcNow);
var options = new FindOneAndUpdateOptions<AccountEntry> { ReturnDocument = ReturnDocument.After };
return await _accountCollection.FindOneAndUpdateAsync(s, filter, update, options, cancellationToken: ct);
}
public async Task<TransEntry> GetTransactionByTidAsync(string tid, CancellationToken ct, IClientSessionHandle s = null)
{
if (s == null)
{
return await _transCollection.Find(x => x.Tid == tid).FirstOrDefaultAsync(ct);
}
return await _transCollection.Find(s, x => x.Tid == tid).FirstOrDefaultAsync(ct);
}
public async Task<List<TransEntry>> GetTransactionsAsync(string accountId, CancellationToken ct, int n = 10)
{
var result = await _transCollection.Find(x => x.Uuid == accountId).SortBy(x => x.CreateTime).Limit(n).ToListAsync(ct);
return result;
}
public async Task<TransResult> RechargeAsync(string accountId, string tid, decimal amount)
{
using (var session = await _mongoClient.StartSessionAsync())
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
{
return await session.WithTransactionAsync(
async (s, ct) =>
{
var existingTransEntry = await GetTransactionByTidAsync(tid, ct, s);
if (existingTransEntry != null)
{
return new TransResult { Code = TransCode.DuplicateTrans };
}
var newBalanceEntry = await AddBalanceAsync(accountId, amount, ct, s);
var newTransEntry = new TransEntry
{
Tid = GenerateTid(),
Uuid = accountId,
Amount = amount,
NewBalance = newBalanceEntry.Balance,
Type = TransType.Recharge,
CreateTime = DateTime.UtcNow,
UpdateTime = DateTime.UtcNow,
};
await _transCollection.InsertOneAsync(s, newTransEntry, cancellationToken: ct);
return new TransResult
{
NewBalance = newTransEntry.NewBalance,
Tid = newTransEntry.Tid
};
}, cancellationToken: cts.Token);
}
}
public async Task<TransResult> RefundAsync(string accountId, string tid, decimal amount)
{
using (var session = await _mongoClient.StartSessionAsync())
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
{
return await session.WithTransactionAsync(
async (s, ct) =>
{
var existingTransEntry = await GetTransactionByTidAsync(tid, ct, s);
if (existingTransEntry != null)
{
return new TransResult { Code = TransCode.DuplicateTrans };
}
var newBalanceEntry = await DecreaseBalanceAsync(accountId, amount, ct, s);
if (newBalanceEntry == null)
{
return new TransResult { Code = TransCode.InsufficientBalance };
}
var newTransEntry = new TransEntry
{
Tid = GenerateTid(),
Uuid = accountId,
Amount = amount,
NewBalance = newBalanceEntry.Balance,
Type = TransType.Refund,
CreateTime = DateTime.UtcNow,
UpdateTime = DateTime.UtcNow,
};
await _transCollection.InsertOneAsync(s, newTransEntry, cancellationToken: ct);
return new TransResult
{
NewBalance = newTransEntry.NewBalance,
Tid = newTransEntry.Tid
};
},
cancellationToken: cts.Token);
}
}
public async Task RunAsync()
{
var random = new Random(42);
// Create 10 accounts
var accountIds = new List<string>();
for (int i = 0; i < 10; i++)
{
var accountId = GenerateAccountUuid();
var account = await CreateAccountAsync(accountId);
accountIds.Add(account.Uuid);
await RechargeAsync(account.Uuid, GenerateTid(), 1000);
Console.WriteLine($"Created account {account.Uuid} with initial balance of 1000.");
}
Console.WriteLine("------------------------------------------------------------------------------");
foreach (var accountId in accountIds)
{
for (int i = 0; i < 5; i++)
{
var isRecharge = random.Next(0, 2) == 0;
var amount = (decimal)(random.NextDouble() * 300);
var tid = GenerateTid();
if (isRecharge)
{
var result = await RechargeAsync(accountId, tid, amount);
if (result.Code == TransCode.Success)
{
Console.WriteLine($"Recharge: Account {accountId}, Amount: {Math.Round(amount, 2)}, New Balance: {Math.Round(result.NewBalance, 2)}, Tid: {tid}");
}
else
{
Console.WriteLine($"Recharge failed: {result.Code}");
}
}
else
{
var result = await RefundAsync(accountId, tid, amount);
if (result.Code == TransCode.Success)
{
Console.WriteLine($"Refund: Account {accountId}, Amount: {Math.Round(amount, 2)}, New Balance: {Math.Round(result.NewBalance, 2)}, Tid: {tid}");
}
else
{
Console.WriteLine($"Refund failed: {result.Code}");
}
}
}
}
Console.WriteLine("------------------------------------------------------------------------------");
foreach (var accountId in accountIds)
{
var balance = await GetBalanceAsync(accountId, CancellationToken.None);
var transactions = await GetTransactionsAsync(accountId, CancellationToken.None);
Console.WriteLine($"Account {accountId} Balance: {Math.Round(balance, 2)}");
Console.WriteLine("Recent Transactions:");
foreach (var transaction in transactions)
{
Console.WriteLine($"Tid: {transaction.Tid}, Type: {transaction.Type}, Amount: {Math.Round(transaction.Amount, 2)}, New Balance: {Math.Round(transaction.NewBalance, 2)}, Time: {transaction.CreateTime}");
}
Console.WriteLine("****************************************************************************");
}
}
}
}