Skip to main content
The Chargeworx platform consists of multiple components that interact through well-defined interfaces. This document describes the communication patterns and integration points between components.

Communication patterns

API to domain layer

The API layer delegates business logic to the domain layer through managers:
// API Controller
[HttpPost]
public async Task<IActionResult> CreateTransaction([FromBody] TransactionRequest request)
{
    var result = await _transactionManager.CreateTransactionAsync(request);
    return Ok(result);
}

// Domain Manager
public async Task<TransactionResponse> CreateTransactionAsync(TransactionRequest request)
{
    // Validate request
    // Call payment processor
    // Store transaction
    // Return response
}
Key characteristics:
  • Controllers are thin, delegating to managers
  • Managers orchestrate business logic
  • DTOs used for request/response
  • Async/await for all I/O operations

Domain to data layer

The domain layer accesses data through stores (repository pattern):
// Domain Manager
public async Task<Transaction> GetTransactionAsync(Guid id)
{
    return await _transactionStore.GetByIdAsync(id);
}

// Data Store
public async Task<Transaction> GetByIdAsync(Guid id)
{
    return await _context.Transactions
        .Include(t => t.TransactionCreditCardEvents)
        .FirstOrDefaultAsync(t => t.Id == id);
}
Key characteristics:
  • Stores encapsulate data access logic
  • Entity Framework Core for ORM
  • Stored procedures for complex queries
  • Transactions for data consistency

Admin UI to API

The Admin UI communicates with the API through typed HTTP clients:
// Admin Controller
public async Task<IActionResult> GetTransactions()
{
    var transactions = await _transactionClient.GetTransactionsAsync();
    return View(transactions);
}

// HTTP Client
public async Task<List<Transaction>> GetTransactionsAsync()
{
    var response = await _httpClient.GetAsync("/api/adm/transactions");
    return await response.Content.ReadAsAsync<List<Transaction>>();
}
Key characteristics:
  • Typed clients for type safety
  • OAuth2 authentication
  • Retry policies for resilience
  • Response caching where appropriate

Payment processor integration

The domain layer integrates with payment processors through service interfaces:
// Domain Manager
public async Task<AuthorizationResponse> AuthorizeAsync(AuthorizationRequest request)
{
    var processor = _processorFactory.GetProcessor(request.ProcessorType);
    return await processor.AuthorizeAsync(request);
}

// Payment Processor Service
public async Task<AuthorizationResponse> AuthorizeAsync(AuthorizationRequest request)
{
    // Build processor-specific request
    // Call processor API
    // Parse response
    // Return standardized response
}
Key characteristics:
  • Factory pattern for processor selection
  • Standardized request/response models
  • Error handling and retry logic
  • Logging of all interactions

Component interaction flows

Transaction processing flow

┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
│  Client  │────▶│   API    │────▶│  Domain  │────▶│   Data   │
└──────────┘     └──────────┘     └──────────┘     └──────────┘
                       │                 │
                       │                 ▼
                       │           ┌──────────┐
                       │           │ Processor│
                       │           └──────────┘
                       │                 │
                       ▼                 ▼
                 ┌──────────┐     ┌──────────┐
                 │ Response │◀────│ Response │
                 └──────────┘     └──────────┘
Step-by-step:
  1. Client sends transaction request to API
  2. API validates request and delegates to domain manager
  3. Domain manager creates transaction record in database
  4. Domain manager calls payment processor service
  5. Processor service sends request to external processor
  6. Processor returns response
  7. Domain manager updates transaction with result
  8. API returns response to client

Account updater flow

┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
│Background│────▶│  Domain  │────▶│   Data   │────▶│   S3     │
│ Process  │     └──────────┘     └──────────┘     └──────────┘
└──────────┘           │                                  │
      │                ▼                                  │
      │          ┌──────────┐                            │
      │          │CyberSource│◀───────────────────────────┘
      │          └──────────┘
      │                │
      │                ▼
      │          ┌──────────┐
      └─────────▶│   Data   │
                 └──────────┘
Step-by-step:
  1. Background process triggers account updater
  2. Domain service queries cards to update from database
  3. Domain service generates CSV file
  4. File uploaded to S3
  5. Domain service uploads file to CyberSource
  6. CyberSource processes batch (24-48 hours)
  7. Background process downloads results
  8. Domain service parses results and updates database

Chargeback processing flow

┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
│Background│────▶│  Domain  │────▶│CyberSource│────▶│   Data   │
│ Process  │     └──────────┘     └──────────┘     └──────────┘
└──────────┘           │                                  │
      │                ▼                                  │
      │          ┌──────────┐                            │
      │          │  Email   │◀───────────────────────────┘
      │          └──────────┘
      │                │
      │                ▼
      │          ┌──────────┐
      └─────────▶│  Admin   │
                 └──────────┘
Step-by-step:
  1. Background process triggers chargeback download
  2. Domain service downloads chargeback report from CyberSource
  3. Domain service parses CSV report
  4. Domain service matches chargebacks to transactions
  5. Domain service creates reversal transactions
  6. Domain service sends email notifications
  7. Admin UI displays chargeback alerts

Inter-service communication

HTTP clients

The platform uses typed HTTP clients for service-to-service communication: Admin clients (Chargeworx.Api.AdminClients)
  • Company management
  • Project management
  • User management
  • Transaction management
  • Report generation
Payment clients (Chargeworx.Api.PaymentClients)
  • Transaction processing
  • Payment info management
  • Charge event processing
Configuration:
services.AddHttpClient<ITransactionClient, TransactionClient>(client =>
{
    client.BaseAddress = new Uri(configuration["ApiBaseUrl"]);
    client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetCircuitBreakerPolicy());

SignalR for real-time updates

The Admin UI uses SignalR for real-time notifications:
// Server-side hub
public class UserMonitorHub : Hub
{
    public async Task SendNotification(string userId, string message)
    {
        await Clients.User(userId).SendAsync("ReceiveNotification", message);
    }
}

// Client-side connection
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/hubs/userMonitor")
    .build();

connection.on("ReceiveNotification", (message) => {
    displayNotification(message);
});
Use cases:
  • Background process status updates
  • Transaction alerts
  • System notifications
  • User activity monitoring

Background processing

Background processes coordinate through a queue-based system:
// Create background process
var process = new BackgroundProcess
{
    Type = BackgroundProcessType.AccountUpdater,
    Status = BackgroundProcessStatus.Pending,
    Priority = 1,
    Payload = JsonConvert.SerializeObject(payload)
};
await _backgroundProcessStore.CreateAsync(process);

// Process execution
var process = await _backgroundProcessStore.GetNextToProcessAsync();
if (process != null)
{
    await ExecuteProcessAsync(process);
}
Process types:
  • Account updater batch processing
  • Chargeback report downloads
  • Transaction report synchronization
  • Data import processing

Database interactions

Multi-database coordination

The platform uses four separate databases with coordinated transactions:
// Transaction spanning multiple databases
using var mainTransaction = await _mainContext.Database.BeginTransactionAsync();
using var keyTransaction = await _keyContext.Database.BeginTransactionAsync();

try
{
    // Create company in main database
    await _mainContext.Companies.AddAsync(company);
    await _mainContext.SaveChangesAsync();
    
    // Create API key in key database
    await _keyContext.Keys.AddAsync(key);
    await _keyContext.SaveChangesAsync();
    
    await mainTransaction.CommitAsync();
    await keyTransaction.CommitAsync();
}
catch
{
    await mainTransaction.RollbackAsync();
    await keyTransaction.RollbackAsync();
    throw;
}

Stored procedures

Complex queries use stored procedures for performance:
// Call stored procedure
var transactions = await _context.Transactions
    .FromSqlRaw("EXEC [dbo].[TransactionGetEntries] @CompanyProjectId, @StartDate, @EndDate",
        new SqlParameter("@CompanyProjectId", projectId),
        new SqlParameter("@StartDate", startDate),
        new SqlParameter("@EndDate", endDate))
    .ToListAsync();

Bulk operations

Import operations use bulk inserts for performance:
// Bulk insert transactions
using var bulkCopy = new SqlBulkCopy(connectionString);
bulkCopy.DestinationTableName = "Transactions";
bulkCopy.BatchSize = 1000;

var dataTable = ConvertToDataTable(transactions);
await bulkCopy.WriteToServerAsync(dataTable);

External service integration

CyberSource integration

SOAP API (legacy transactions):
var client = new TransactionProcessorClient();
var request = BuildSoapRequest(transaction);
var response = await client.runTransactionAsync(request);
REST API (modern features):
var client = new HttpClient();
client.DefaultRequestHeaders.Add("v-c-merchant-id", merchantId);
client.DefaultRequestHeaders.Add("Signature", GenerateSignature(request));

var response = await client.PostAsync(url, content);

PayPal Payflow integration

var request = new Dictionary<string, string>
{
    ["TRXTYPE"] = "A",
    ["TENDER"] = "C",
    ["AMT"] = amount.ToString("F2"),
    ["ACCT"] = cardNumber
};

var response = await _httpClient.PostAsync(payflowUrl, 
    new FormUrlEncodedContent(request));

AWS services

S3 for file storage:
var s3Client = new AmazonS3Client();
await s3Client.PutObjectAsync(new PutObjectRequest
{
    BucketName = bucketName,
    Key = fileName,
    InputStream = fileStream
});
SES for email:
var sesClient = new AmazonSimpleEmailServiceClient();
await sesClient.SendEmailAsync(new SendEmailRequest
{
    Source = fromAddress,
    Destination = new Destination { ToAddresses = toAddresses },
    Message = new Message
    {
        Subject = new Content(subject),
        Body = new Body { Html = new Content(htmlBody) }
    }
});

Error handling and resilience

Retry policies

HTTP clients use Polly for retry logic:
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .WaitAndRetryAsync(3, retryAttempt => 
            TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}

Circuit breaker

Prevent cascading failures with circuit breaker:
static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
}

Exception handling

Centralized exception handling in middleware:
app.UseExceptionHandler(errorApp =>
{
    errorApp.Run(async context =>
    {
        var error = context.Features.Get<IExceptionHandlerFeature>();
        var exception = error?.Error;
        
        // Log exception
        _logger.LogError(exception, "Unhandled exception");
        
        // Return error response
        context.Response.StatusCode = 500;
        await context.Response.WriteAsJsonAsync(new ErrorDetails
        {
            Message = "An error occurred processing your request",
            CorrelationId = context.TraceIdentifier
        });
    });
});

Caching strategy

Response caching

API endpoints use response caching:
[HttpGet]
[ResponseCache(Duration = 300, VaryByQueryKeys = new[] { "id" })]
public async Task<IActionResult> GetCompany(Guid id)
{
    var company = await _companyManager.GetByIdAsync(id);
    return Ok(company);
}

Distributed caching

Session state and frequently accessed data use distributed cache:
// Store in cache
await _cache.SetStringAsync(key, JsonConvert.SerializeObject(value),
    new DistributedCacheEntryOptions
    {
        AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
    });

// Retrieve from cache
var cached = await _cache.GetStringAsync(key);
if (cached != null)
{
    return JsonConvert.DeserializeObject<T>(cached);
}

Security considerations

Authentication flow

┌──────────┐     ┌──────────┐     ┌──────────┐
│  Client  │────▶│   API    │────▶│Identity  │
└──────────┘     └──────────┘     │ Server   │
      │                │           └──────────┘
      │                │                 │
      │                ◀─────────────────┘
      │                │
      ◀────────────────┘
OAuth2 flow:
  1. Client requests token from IdentityServer
  2. IdentityServer validates credentials
  3. IdentityServer returns access token
  4. Client includes token in API requests
  5. API validates token and processes request

Data encryption

Sensitive data is encrypted at rest:
// Encrypt credit card number
var encrypted = _aesHelper.Encrypt(cardNumber, encryptionKey);

// Decrypt credit card number
var decrypted = _aesHelper.Decrypt(encrypted, encryptionKey);

Next steps