Skip to content
Merged
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
50 changes: 50 additions & 0 deletions TransactionProcessing.MerchantPos/Pages/Config.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
@page
@model TransactionProcessing.MerchantPos.Pages.ConfigModel
@{
ViewData["Title"] = "Configuration";
}

<main class="screen-shell">
<section class="screen-header">
<div>
<h1 class="screen-title">Configuration</h1>
<p class="section-subtitle">Edit app settings and external SQLite paths from a standard form.</p>
</div>
</section>

@if (!string.IsNullOrWhiteSpace(Model.Message))
{
<div class="message">@Model.Message</div>
}

<form method="post" asp-page-handler="Save" class="panel config-panel">
<div class="grid-2">
<section class="group">
<h2>Service and API settings</h2>
<div class="fields fields-2">
<div class="field"><label asp-for="Config.SecurityService"></label><input asp-for="Config.SecurityService" /></div>
<div class="field"><label asp-for="Config.TransactionProcessorACL"></label><input asp-for="Config.TransactionProcessorACL" /></div>
<div class="field"><label asp-for="Config.TransactionProcessorApi"></label><input asp-for="Config.TransactionProcessorApi" /></div>
<div class="field"><label asp-for="Config.TestHost"></label><input asp-for="Config.TestHost" /></div>
<div class="field"><label asp-for="Config.ClientId"></label><input asp-for="Config.ClientId" /></div>
<div class="field"><label asp-for="Config.ClientSecret"></label><input asp-for="Config.ClientSecret" type="text" autocomplete="off" spellcheck="false" /></div>
<div class="field"><label asp-for="Config.ServiceClientId"></label><input asp-for="Config.ServiceClientId" /></div>
<div class="field"><label asp-for="Config.ServiceClientSecret"></label><input asp-for="Config.ServiceClientSecret" type="text" autocomplete="off" spellcheck="false" /></div>
</div>
</section>

<section class="group">
<h2>Storage paths</h2>
<div class="fields storage-fields">
<div class="field"><label asp-for="Config.MerchantDb"></label><input asp-for="Config.MerchantDb" /></div>
<div class="field"><label asp-for="Config.SettingsDb"></label><input asp-for="Config.SettingsDb" /></div>
</div>
<p class="page-note" style="margin-top: 12px;">Keep both paths outside the binaries folder so the SQLite files remain writable at runtime.</p>
</section>
</div>

<div class="button-row">
<button type="submit" class="button">Save configuration</button>
</div>
</form>
</main>
87 changes: 87 additions & 0 deletions TransactionProcessing.MerchantPos/Pages/Config.cshtml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using TransactionProcessing.MerchantPos.Persistence;
using TransactionProcessing.MerchantPos.Runtime;

namespace TransactionProcessing.MerchantPos.Pages;

public sealed class ConfigModel : PageModel
{
private const string SecretMask = "*****";

Check failure on line 10 in TransactionProcessing.MerchantPos/Pages/Config.cshtml.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

TransactionProcessing.MerchantPos/Pages/Config.cshtml.cs#L10

Hardcoded passwords are a security risk.
private readonly MerchantPosSettingsStore _settingsStore;

public ConfigModel(MerchantPosSettingsStore settingsStore)
{
_settingsStore = settingsStore;
}

[BindProperty(SupportsGet = true)]
public string? Message { get; set; }

[BindProperty]
public ConfigInput Config { get; set; } = new();

public void OnGet()
{
ViewData["ActiveNav"] = "config";
ViewData["Title"] = "Configuration";
Config = ConfigInput.FromSnapshot(_settingsStore.Current);
Config.ClientSecret = MaskSecret(Config.ClientSecret);
Config.ServiceClientSecret = MaskSecret(Config.ServiceClientSecret);
}

public async Task<IActionResult> OnPostSaveAsync()
{
var settings = _settingsStore.Current;
settings.ApiConfiguration.SecurityService = Config.SecurityService;
settings.ApiConfiguration.TransactionProcessorACL = Config.TransactionProcessorACL;
settings.ApiConfiguration.TransactionProcessorApi = Config.TransactionProcessorApi;
settings.ApiConfiguration.TestHost = Config.TestHost;

settings.WorkerSettings.ClientId = Config.ClientId;
settings.WorkerSettings.ClientSecret = UnmaskSecret(Config.ClientSecret, settings.WorkerSettings.ClientSecret);
settings.WorkerSettings.ServiceClientId = Config.ServiceClientId;
settings.WorkerSettings.ServiceClientSecret = UnmaskSecret(Config.ServiceClientSecret, settings.WorkerSettings.ServiceClientSecret);

settings.ConnectionStrings.MerchantDb = MerchantPosSettingsStore.NormalizePath(Config.MerchantDb);
settings.ConnectionStrings.SettingsDb = MerchantPosSettingsStore.NormalizePath(Config.SettingsDb);

await _settingsStore.SaveAsync(settings);
return RedirectToPage("/Config", new { message = "Configuration saved" });
}

private static string MaskSecret(string? value)
=> string.IsNullOrWhiteSpace(value) ? string.Empty : SecretMask;

private static string UnmaskSecret(string? value, string currentValue)
=> string.IsNullOrWhiteSpace(value) || string.Equals(value, SecretMask, StringComparison.Ordinal) ? currentValue : value;

public sealed class ConfigInput
{
public string SecurityService { get; set; } = string.Empty;
public string TransactionProcessorACL { get; set; } = string.Empty;
public string TransactionProcessorApi { get; set; } = string.Empty;
public string TestHost { get; set; } = string.Empty;
public string ClientId { get; set; } = string.Empty;
public string ClientSecret { get; set; } = string.Empty;
public string ServiceClientId { get; set; } = string.Empty;
public string ServiceClientSecret { get; set; } = string.Empty;
public string MerchantDb { get; set; } = string.Empty;
public string SettingsDb { get; set; } = string.Empty;

public static ConfigInput FromSnapshot(MerchantPosSettingsSnapshot snapshot)
=> new()
{
SecurityService = snapshot.ApiConfiguration.SecurityService,
TransactionProcessorACL = snapshot.ApiConfiguration.TransactionProcessorACL,
TransactionProcessorApi = snapshot.ApiConfiguration.TransactionProcessorApi,
TestHost = snapshot.ApiConfiguration.TestHost,
ClientId = snapshot.WorkerSettings.ClientId,
ClientSecret = snapshot.WorkerSettings.ClientSecret,
ServiceClientId = snapshot.WorkerSettings.ServiceClientId,
ServiceClientSecret = snapshot.WorkerSettings.ServiceClientSecret,
MerchantDb = MerchantPosSettingsStore.NormalizePath(snapshot.ConnectionStrings.MerchantDb),
SettingsDb = MerchantPosSettingsStore.NormalizePath(snapshot.ConnectionStrings.SettingsDb)
};
}
}
105 changes: 105 additions & 0 deletions TransactionProcessing.MerchantPos/Pages/Index.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
@page
@model TransactionProcessing.MerchantPos.Pages.IndexModel
@{
ViewData["Title"] = "Merchant POS Dashboard";
}
@section Head {
<meta http-equiv="refresh" content="5" />
}

<main class="page">
<section class="hero">
<div class="panel hero-main">
<div>
<div class="eyebrow"><span class="status-dot"></span> Merchant POS Control Centre</div>
<h1 class="title">Running stats, live health, and operational context.</h1>
<p class="subtitle">This page stays focused on live status. Merchant editing and configuration are separate screens so the dashboard stays readable.</p>
</div>

<div class="chip-row">
<div class="chip">Last updated: @Model.Dashboard.GeneratedUtc.ToLocalTime().ToString("HH:mm:ss")</div>
<div class="chip">Settings DB: @Model.Dashboard.SettingsDbPath</div>
<div class="chip">Mode: Operations console</div>
</div>
</div>
</section>

<section class="stats-grid">
<div class="stat">
<div class="stat-label">Merchants</div>
<div class="stat-value">@Model.Dashboard.Summary.MerchantCount</div>
<div class="stat-foot">Configured and observed</div>
</div>
<div class="stat">
<div class="stat-label">Balance</div>
<div class="stat-value">@Model.Dashboard.Summary.TotalBalance.ToString("0.00")</div>
<div class="stat-foot">Across all merchants</div>
</div>
<div class="stat">
<div class="stat-label">Sales today</div>
<div class="stat-value">@Model.Dashboard.Summary.SalesCount</div>
<div class="stat-foot">Successful transactions</div>
</div>
<div class="stat">
<div class="stat-label">Failures</div>
<div class="stat-value">@Model.Dashboard.Summary.FailedSales</div>
<div class="stat-foot">Rejected or errored sales</div>
</div>
</section>

<section class="panel page">
<div class="section-header">
<div>
<h2 class="section-title">Live merchant board</h2>
<p class="section-subtitle">Refreshes automatically every five seconds.</p>
</div>
<div class="button-row">
<a class="button secondary" href="/merchants">Manage merchants</a>
<a class="button secondary" href="/config">Open config</a>
</div>
</div>

<div class="table-wrap">
<table class="table">
<thead>
<tr>
<th>Merchant</th>
<th>Balance</th>
<th>Sales</th>
<th>Failures</th>
<th>Last sale</th>
<th>Last EOD</th>
</tr>
</thead>
<tbody>
@if (Model.Dashboard.Merchants.Count == 0)
{
<tr><td colspan="6">No merchants configured yet.</td></tr>
}
else
{
foreach (var row in Model.Dashboard.Merchants)
{
<tr>
<td>@row.MerchantName</td>
<td>@row.Balance.ToString("0.00")</td>
<td>@row.Sales</td>
<td>@row.FailedSales</td>
<td>
<span class="badge @(row.LastSaleUtc is null ? "off" : string.Empty)">
@(row.LastSaleUtc is null ? "Never" : row.LastSaleUtc.Value.ToLocalTime().ToString("dd MMM HH:mm"))
</span>
</td>
<td>
<span class="badge @(row.LastEndOfDayUtc is null ? "warn" : string.Empty)">
@(row.LastEndOfDayUtc is null ? "Never" : row.LastEndOfDayUtc.Value.ToLocalTime().ToString("dd MMM HH:mm"))
</span>
</td>
</tr>
}
}
</tbody>
</table>
</div>
</section>
</main>
24 changes: 24 additions & 0 deletions TransactionProcessing.MerchantPos/Pages/Index.cshtml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
using TransactionProcessing.MerchantPos.Runtime;
using TransactionProcessing.MerchantPos.Web;

namespace TransactionProcessing.MerchantPos.Pages;

public sealed class IndexModel : PageModel
{
private readonly MerchantDashboardModelFactory _dashboardFactory;

public IndexModel(MerchantDashboardModelFactory dashboardFactory)
{
_dashboardFactory = dashboardFactory;
}

public MerchantDashboardViewModel Dashboard { get; private set; } = default!;

public async Task OnGetAsync()
{
ViewData["ActiveNav"] = "dashboard";
ViewData["Title"] = "Merchant POS Dashboard";
Dashboard = await _dashboardFactory.BuildAsync();
}
}
102 changes: 102 additions & 0 deletions TransactionProcessing.MerchantPos/Pages/Merchants.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
@page
@model TransactionProcessing.MerchantPos.Pages.MerchantsModel
@{
ViewData["Title"] = "Merchant Management";
}

<main class="screen-shell">
<section class="screen-header">
<div>
<h1 class="screen-title">Merchant management</h1>
<p class="section-subtitle">Pick one merchant, edit it, and save with a normal form submit.</p>
</div>
</section>

@if (!string.IsNullOrWhiteSpace(Model.Message))
{
<div class="message">@Model.Message</div>
}

<section class="two-column">
<aside class="panel side-panel">
<div class="panel-title">Merchants</div>
<div class="merchant-list">
@if (Model.Merchants.Count == 0)
{
<div class="page-note">No merchants configured yet.</div>
}
else
{
for (var i = 0; i < Model.Merchants.Count; i++)
{
var merchant = Model.Merchants[i];
<a class="merchant-link @(i == Model.Selected ? "active" : string.Empty)" href="/merchants?selected=@i">
@merchant.MerchantName
<small>@(merchant.Enabled ? "Enabled" : "Disabled") · @merchant.MerchantId</small>
</a>
}
}
</div>
<form method="post" asp-page-handler="Add" class="stacked-actions">
<button type="submit" class="button">Add merchant</button>
</form>
</aside>

<section class="panel editor-panel">
@if (Model.Merchants.Count == 0 && !Model.IsDraft)
{
<div class="editor-shell">
<div class="editor-summary">
<div class="page-note">Use <strong>Add merchant</strong> to create the first merchant.</div>
</div>
</div>
}
else
{
<form method="post" asp-page-handler="Save" class="editor-shell">
<input type="hidden" asp-for="Selected" />
<div class="editor-summary">
<h2 class="screen-title">@Model.Merchant.MerchantName</h2>
<div class="page-note">@(Model.IsDraft ? "This is a new merchant draft. Nothing is written to the database until you save it." : "The edit screen stays on one page and saves with a standard form submit.")</div>
</div>
<div class="fields merchant-fields">
<div class="field field-wide"><label asp-for="Merchant.MerchantName"></label><input asp-for="Merchant.MerchantName" /></div>
<div class="fields fields-2 field-wide">
<div class="field"><label asp-for="Merchant.MerchantId"></label><input asp-for="Merchant.MerchantId" /></div>
<div class="field"><label asp-for="Merchant.EstateId"></label><input asp-for="Merchant.EstateId" /></div>
</div>
<div class="fields fields-2 field-wide">
<div class="field"><label asp-for="Merchant.Username"></label><input asp-for="Merchant.Username" /></div>
<div class="field"><label asp-for="Merchant.Password"></label><input asp-for="Merchant.Password" type="text" autocomplete="off" spellcheck="false" /></div>
</div>
<div class="fields fields-3 field-wide">
<div class="field"><label asp-for="Merchant.ApplicationVersion"></label><input asp-for="Merchant.ApplicationVersion" /></div>
<div class="field"><label asp-for="Merchant.DeviceIdentifier"></label><input asp-for="Merchant.DeviceIdentifier" /></div>
<div class="field"><label asp-for="Merchant.SaleIntervalSeconds"></label><input asp-for="Merchant.SaleIntervalSeconds" type="number" /></div>
</div>
<div class="fields fields-3 field-wide">
<div class="field"><label asp-for="Merchant.FailureInjectionProbability"></label><input asp-for="Merchant.FailureInjectionProbability" type="number" step="0.01" /></div>
<div class="field"><label asp-for="Merchant.DepositThreshold"></label><input asp-for="Merchant.DepositThreshold" type="number" step="0.01" /></div>
<div class="field"><label asp-for="Merchant.DepositAmount"></label><input asp-for="Merchant.DepositAmount" type="number" step="0.01" /></div>
</div>
<div class="fields fields-2 field-wide">
<div class="field"><label asp-for="Merchant.OpeningTime"></label><input asp-for="Merchant.OpeningTime" type="time" /></div>
<div class="field"><label asp-for="Merchant.ClosingTime"></label><input asp-for="Merchant.ClosingTime" type="time" /></div>
</div>
</div>
<label class="checkbox-row">
<input asp-for="Merchant.Enabled" />
<span class="checkbox-label">Enabled</span>
</label>
<div class="button-row">
@if (!Model.IsDraft)
{
<button type="submit" class="button secondary" asp-page-handler="Remove">Remove merchant</button>
}
<button type="submit" class="button">Save merchant</button>
</div>
</form>
}
</section>
</section>
</main>
Loading
Loading