Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
# TestHosts

## Mobile Wallet test API

The `TestHosts` web application now includes a GSMA-style mobile wallet test API under `mobilemoney/v1_0` with:

- OAuth2 client-credentials token issuance via `POST /oauth/token`
- account creation, lookup, status, balance, and transaction history endpoints
- idempotent transaction and reversal creation using the `X-Idempotency-Key` header
- asynchronous transaction and reversal completion via a background processor
- webhook subscriptions plus per-request callback URLs for event delivery
- audit entries for token, account, transaction, reversal, and webhook operations
- SQL Server or in-memory persistence using the `MobileWalletReadModel` connection string

Default seeded OAuth2 client credentials are configured in `appsettings*.json` under `MobileWallet:OAuth`.
867 changes: 867 additions & 0 deletions TestHosts/TestHosts/Controllers/MobileWalletController.cs

Large diffs are not rendered by default.

189 changes: 189 additions & 0 deletions TestHosts/TestHosts/Database/MobileWallet/MobileWalletContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
namespace TestHosts.Database.MobileWallet
{
using Microsoft.EntityFrameworkCore;
using Shared.General;
using System;

internal static class MobileWalletStatus
{
internal const String Pending = "pending";
internal const String Completed = "completed";
internal const String Failed = "failed";
internal const String Rejected = "rejected";
internal const String Reversed = "reversed";
internal const String Active = "active";
}

public class MobileWalletContext : DbContext
{
private readonly String ConnectionString;

public MobileWalletContext()
{
this.ConnectionString = ConfigurationReader.GetConnectionString(Constants.MobileWalletReadModelConfig);
}

public MobileWalletContext(String connectionString)
{
this.ConnectionString = connectionString;
}

public MobileWalletContext(DbContextOptions<MobileWalletContext> dbContextOptions) : base(dbContextOptions)
{
}

public DbSet<MobileWalletClient> Clients { get; set; }
public DbSet<MobileWalletAccessToken> AccessTokens { get; set; }
public DbSet<MobileWalletAccount> Accounts { get; set; }
public DbSet<MobileWalletTransaction> Transactions { get; set; }
public DbSet<MobileWalletReversal> Reversals { get; set; }
public DbSet<MobileWalletWebhookSubscription> WebhookSubscriptions { get; set; }
public DbSet<MobileWalletWebhookDelivery> WebhookDeliveries { get; set; }
public DbSet<MobileWalletAuditEntry> AuditEntries { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (string.IsNullOrWhiteSpace(this.ConnectionString) == false)
{
optionsBuilder.UseSqlServer(this.ConnectionString);
}

base.OnConfiguring(optionsBuilder);
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<MobileWalletClient>().HasKey(c => c.ClientId);
modelBuilder.Entity<MobileWalletAccessToken>().HasKey(t => t.TokenId);
modelBuilder.Entity<MobileWalletAccessToken>().HasIndex(t => t.Token).IsUnique();
modelBuilder.Entity<MobileWalletAccount>().HasKey(a => a.AccountReference);
modelBuilder.Entity<MobileWalletTransaction>().HasKey(t => t.TransactionReference);
modelBuilder.Entity<MobileWalletTransaction>().HasIndex(t => new { t.ClientId, t.IdempotencyKey }).IsUnique();
modelBuilder.Entity<MobileWalletTransaction>().HasIndex(t => t.ExternalReference);
modelBuilder.Entity<MobileWalletReversal>().HasKey(r => r.ReversalReference);
modelBuilder.Entity<MobileWalletReversal>().HasIndex(r => new { r.ClientId, r.IdempotencyKey }).IsUnique();
modelBuilder.Entity<MobileWalletWebhookSubscription>().HasKey(s => s.SubscriptionId);
modelBuilder.Entity<MobileWalletWebhookDelivery>().HasKey(d => d.DeliveryId);
modelBuilder.Entity<MobileWalletAuditEntry>().HasKey(a => a.AuditEntryId);

base.OnModelCreating(modelBuilder);
}
}

public sealed class MobileWalletClient
{
public String ClientId { get; set; } = String.Empty;
public String ClientSecret { get; set; } = String.Empty;
public String Name { get; set; } = String.Empty;
public String? CallbackUrl { get; set; }
public Boolean IsActive { get; set; }
public DateTime CreatedAt { get; set; }
}

public sealed class MobileWalletAccessToken
{
public Guid TokenId { get; set; }
public String Token { get; set; } = String.Empty;
public String ClientId { get; set; } = String.Empty;
public DateTime ExpiresAt { get; set; }
public DateTime CreatedAt { get; set; }
}

public sealed class MobileWalletAccount
{
public String AccountReference { get; set; } = String.Empty;
public String AccountType { get; set; } = "wallet";
public String Status { get; set; } = MobileWalletStatus.Active;
public String Currency { get; set; } = "USD";
public Decimal AvailableBalance { get; set; }
public String GivenName { get; set; } = String.Empty;
public String FamilyName { get; set; } = String.Empty;
public String? Msisdn { get; set; }
public String? EmailAddress { get; set; }
public String KycStatus { get; set; } = "not_provided";
public String? IdentityType { get; set; }
public String? IdentityNumber { get; set; }
public String? DateOfBirth { get; set; }
public String? Nationality { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}

public sealed class MobileWalletTransaction
{
public String TransactionReference { get; set; } = String.Empty;
public String ClientId { get; set; } = String.Empty;
public String TransactionType { get; set; } = String.Empty;
public String Status { get; set; } = MobileWalletStatus.Pending;
public String StatusMessage { get; set; } = "Transaction accepted for asynchronous processing.";
public Decimal Amount { get; set; }
public String Currency { get; set; } = "USD";
public String DebitAccountReference { get; set; } = String.Empty;
public String CreditAccountReference { get; set; } = String.Empty;
public String? ExternalReference { get; set; }
public String IdempotencyKey { get; set; } = String.Empty;
public String RequestPayload { get; set; } = String.Empty;
public String? CallbackUrl { get; set; }
public String? MetadataJson { get; set; }
public Boolean BalanceApplied { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public DateTime? CompletedAt { get; set; }
}

public sealed class MobileWalletReversal
{
public String ReversalReference { get; set; } = String.Empty;
public String ClientId { get; set; } = String.Empty;
public String OriginalTransactionReference { get; set; } = String.Empty;
public String Reason { get; set; } = String.Empty;
public String Status { get; set; } = MobileWalletStatus.Pending;
public String StatusMessage { get; set; } = "Reversal accepted for asynchronous processing.";
public String IdempotencyKey { get; set; } = String.Empty;
public String RequestPayload { get; set; } = String.Empty;
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public DateTime? CompletedAt { get; set; }
}

public sealed class MobileWalletWebhookSubscription
{
public Guid SubscriptionId { get; set; }
public String ClientId { get; set; } = String.Empty;
public String EventType { get; set; } = "*";
public String CallbackUrl { get; set; } = String.Empty;
public Boolean IsActive { get; set; }
public DateTime CreatedAt { get; set; }
}

public sealed class MobileWalletWebhookDelivery
{
public Guid DeliveryId { get; set; }
public String ClientId { get; set; } = String.Empty;
public String EventType { get; set; } = String.Empty;
public String ResourceReference { get; set; } = String.Empty;
public String CallbackUrl { get; set; } = String.Empty;
public String Payload { get; set; } = String.Empty;
public String Status { get; set; } = MobileWalletStatus.Pending;
public Int32 AttemptCount { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? LastAttemptAt { get; set; }
public DateTime? DeliveredAt { get; set; }
public String? LastError { get; set; }
}

public sealed class MobileWalletAuditEntry
{
public Guid AuditEntryId { get; set; }
public String ResourceType { get; set; } = String.Empty;
public String ResourceReference { get; set; } = String.Empty;
public String Action { get; set; } = String.Empty;
public String Actor { get; set; } = String.Empty;
public String HttpMethod { get; set; } = String.Empty;
public String Path { get; set; } = String.Empty;
public String? IdempotencyKey { get; set; }
public String? RequestPayload { get; set; }
public String? ResponsePayload { get; set; }
public DateTime CreatedAt { get; set; }
}
}
Loading
Loading