Skip to content

Catalog wallet multi-currency + currency_type linkage#7

Merged
billsonnn merged 11 commits intonitrodevco:mainfrom
Diddyy:feature/catalog-base-with-guardrails-autoreload
Feb 8, 2026
Merged

Catalog wallet multi-currency + currency_type linkage#7
billsonnn merged 11 commits intonitrodevco:mainfrom
Diddyy:feature/catalog-base-with-guardrails-autoreload

Conversation

@Diddyy
Copy link
Contributor

@Diddyy Diddyy commented Feb 7, 2026

Summary

  • add currency_type lookup + protocol-id based currency modeling
  • link player_currencies to currency_type (currency_type_id FK + backfill)
  • implement wallet debit/refund primitives for multi-currency support
  • wire catalog purchase flow to debit credits + configured activity-point currency
  • improve purchase error routing and misconfiguration logging
  • send wallet composers conditionally after purchase

Dependency Context

This branch is built on top of the work in:

Those PRs are currently open. Please review/merge in dependency order first, then this PR.

Validation

  • dotnet build Turbo.Main/Turbo.Main.csproj -t:TurboCloudFastCheck
  • dotnet build Turbo.Main/Turbo.Main.csproj -t:TurboCloudQualityGate

Copilot AI review requested due to automatic review settings February 7, 2026 20:29
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds multi-currency wallet support to Turbo Cloud’s catalog purchase flow by introducing a currency_type lookup model, linking player_currencies to it, and implementing wallet debit/refund primitives. The branch also includes developer-experience and repo governance additions (bootstrap scripts, quality gates, plugin hot reload, and architecture/AI contract docs), consistent with the noted dependency PRs.

Changes:

  • Add currency_type modeling + FK linkage/backfill for player_currencies, and use protocol-id based currency identifiers.
  • Implement wallet debit/refund via a new IPlayerWalletGrain, and wire catalog purchasing to debit/refund with improved error routing.
  • Add developer tooling/docs: bootstrap scripts, pinned SDK, repo quality targets/hooks, and (dev-only) plugin hot reload.

Reviewed changes

Copilot reviewed 72 out of 73 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
scripts/bootstrap.sh Adds *nix bootstrap to validate layout, set hooks, create dev settings, and build.
scripts/bootstrap.ps1 Adds PowerShell bootstrap equivalent.
global.json Pins .NET SDK version/roll-forward policy.
docs/patterns/UnitTestPattern.cs Adds reference unit test sample pattern.
docs/patterns/ServicePattern.cs Adds reference service orchestration pattern.
docs/patterns/HandlerPattern.cs Adds reference packet handler pattern.
docs/patterns/README.md Documents available pattern samples.
docs/orleans.md Adds Orleans architecture/usage notes for contributors.
Turbo.Runtime/AsyncSignal.cs Suppresses analyzer warning for the existing WaitAsync pattern.
Turbo.Rooms/Wired/WiredPolicy.cs Removes unused field.
Turbo.Primitives/Players/Wallet/WalletDebitResult.cs Adds wallet debit result contract.
Turbo.Primitives/Players/Wallet/WalletDebitRequest.cs Adds wallet debit request contract.
Turbo.Primitives/Players/Wallet/WalletDebitFailure.cs Adds wallet debit failure contract.
Turbo.Primitives/Players/Wallet/WalletCurrencyKind.cs Adds enum for wallet currency kinds.
Turbo.Primitives/Players/Wallet/WalletCurrencyKeyMapper.cs Adds canonicalization + equivalent-key mapping for currency keys.
Turbo.Primitives/Players/IPlayerGrain.cs Adds GetWalletAsync to player grain contract.
Turbo.Primitives/Players/Grains/IPlayerWalletGrain.cs Introduces wallet grain interface for debit/refund.
Turbo.Primitives/Orleans/Snapshots/Players/PlayerWalletSnapshot.cs Adds wallet snapshot for credits/AP/emeralds/silver.
Turbo.Primitives/Orleans/GrainFactoryExtensions.cs Adds GetPlayerWalletGrain helpers.
Turbo.Primitives/Messages/Outgoing/Users/IgnoredUsersMessageComposer.cs Implements payload shape for ignored users composer.
Turbo.Primitives/Messages/Outgoing/Perk/PerkAllowancesMessageComposer.cs Implements perk allowances composer + item model.
Turbo.Primitives/Messages/Outgoing/Notifications/MOTDNotificationEventMessageComposer.cs Implements MOTD payload shape.
Turbo.Primitives/Messages/Outgoing/Nft/UserNftChatStylesMessageComposer.cs Implements NFT chat styles payload shape.
Turbo.Primitives/Messages/Outgoing/Inventory/Badges/BadgePointLimitsEventMessageComposer.cs Implements badge point limits payload model.
Turbo.Primitives/Messages/Outgoing/Groupforums/UnreadForumsCountMessageComposer.cs Implements unread forums count payload.
Turbo.Primitives/Messages/Outgoing/Catalog/NotEnoughBalanceMessageComposer.cs Adds structured “not enough balance” payload for purchase failures.
Turbo.Primitives/Messages/Incoming/Users/BlockListInitMessage.cs Adds missing incoming message contract.
Turbo.Primitives/Messages/Incoming/Room/Engine/ClickCharacterMessage.cs Adds missing incoming message contract.
Turbo.Primitives/Messages/Incoming/Quest/GetDailyTasksMessage.cs Adds missing incoming message contract.
Turbo.Primitives/Catalog/Snapshots/CatalogCurrencyTypeSnapshot.cs Adds currency type snapshot for catalog lookups.
Turbo.Primitives/Catalog/Providers/ICatalogCurrencyTypeProvider.cs Adds provider contract for currency type lookups/reload.
Turbo.Primitives/Catalog/Enums/CatalogPurchaseErrorType.cs Extends purchase error types for balance/misconfig/failure cases.
Turbo.Primitives/Catalog/CatalogBalanceFailure.cs Adds structured balance failure details for error routing.
Turbo.Plugins/PluginManager.cs Adds dev-plugin discovery, reload gating, and single-plugin reload flow.
Turbo.Plugins/PluginHotReloadService.cs Adds dev-only file watcher to trigger plugin reloads.
Turbo.Plugins/Extensions/ServiceCollectionExtensions.cs Wires plugin config and dev-only hot reload registration.
Turbo.Plugins/Configuration/PluginConfig.cs Adds debounce and dev plugin paths to plugin config.
Turbo.Players/Grains/PlayerWalletGrain.cs Implements debit/refund against player_currencies with EF Core updates + transactions.
Turbo.Players/Grains/PlayerGrain.cs Implements GetWalletAsync snapshot generation from DB currency rows.
Turbo.PacketHandlers/Nft/GetSilverMessageHandler.cs Sends silver balance from wallet snapshot instead of constant 0.
Turbo.PacketHandlers/Nft/GetNftCreditsMessageHandler.cs Sends emerald balance from wallet snapshot instead of constant 0.
Turbo.PacketHandlers/Inventory/Purse/GetCreditsInfoMessageHandler.cs Sends credits/AP balances from wallet snapshot.
Turbo.PacketHandlers/Handshake/SSOTicketMessageHandler.cs Loads wallet snapshot and sends activity points + perk allowances on login.
Turbo.PacketHandlers/Catalog/PurchaseFromCatalogMessageHandler.cs Updates success path to send updated balances; routes balance failures to NotEnoughBalance composer.
Turbo.Main/Turbo.Main.csproj Makes appsettings env files conditional on existence (avoids missing-file build issues).
Turbo.Main/Migrations/TurboDbContextModelSnapshot.cs Updates EF model snapshot for currency_type + new relations/indexes.
Turbo.Main/Migrations/20260207223000_LinkPlayerCurrenciesToCurrencyType.cs Adds currency_type table, links player_currencies, and backfills ids/rows.
Turbo.Main/Console/ConsoleCommandService.cs Implements reload-plugins and reload-plugin console commands.
Turbo.Database/Entities/Players/PlayerCurrencyEntity.cs Adds FK to currency_type and unique index per player+currency_type_id.
Turbo.Database/Entities/Catalog/CurrencyTypeEntity.cs Adds EF entity for currency_type.
Turbo.Database/Entities/Catalog/CatalogOfferEntity.cs Adds navigation to CurrencyTypeEntity.
Turbo.Database/Context/TurboDbContext.cs Adds DbSet for CurrencyTypes + configures id generation behavior.
Turbo.Database/Context/ITurboDbContext.cs Adds CurrencyTypes to context interface.
Turbo.Catalog/Providers/CatalogSnapshotProvider.cs Minor formatting change in snapshot generation.
Turbo.Catalog/Providers/CatalogCurrencyTypeProvider.cs Implements cached DB-backed currency type provider.
Turbo.Catalog/Grains/CatalogPurchaseGrain.cs Debits wallet before grant; refunds on grant failure; adds misconfig logging + improved error mapping.
Turbo.Catalog/Exceptions/CatalogPurchaseException.cs Adds balance failure + inner exception routing.
Turbo.Catalog/CatalogModule.cs Registers CatalogCurrencyTypeProvider.
README.md Adds quickstart, quality gate docs, plugin dev/hot reload docs, AI-assist pointers.
Directory.Build.targets Adds TurboCloudFastCheck + TurboCloudQualityGate targets.
Directory.Build.props Adds AI policy phase controls + Orleans warnings-as-errors configuration.
CONTRIBUTING.md Adds contributor workflow and validation commands.
CONTEXT.md Adds architecture boundaries and placement rules.
CODEX.md Adds Codex adapter pointing to canonical AI contract.
CLAUDE.md Adds Claude adapter pointing to canonical AI contract.
AGENTS.md Adds canonical AI coding/review contract and validation requirements.
.gitignore Stops ignoring scripts folder; keeps plugins-shadow ignore.
.github/workflows/quality.yml Adds CI quality gate workflow on PR/push.
.github/copilot-instructions.md Adds Copilot adapter pointing to canonical AI contract and repo constraints.
.githooks/pre-push Adds pre-push quality gate hook.
.githooks/pre-commit Adds pre-commit fast check hook.
.gitattributes Enforces LF endings for key text formats/hooks.
.config/dotnet-tools.json Updates csharpier tool version.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +66 to +70
if (seen.Add(manifest.Key))
{
list.Add((manifest, dir));
_logger.LogDebug("Discovered dev plugin {Key} from {Dir}", manifest.Key, dir);
}
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DiscoverPlugins() uses a seen set to dedupe plugin keys across DevPluginPaths and the runtime plugin folder, but when a duplicate key is encountered it is silently skipped. This doesn't match the behavior described in README.md (dev path wins + a warning is logged). Consider logging a warning when seen.Add(manifest.Key) is false (and include both paths) so users can diagnose why a plugin didn't load.

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +13
<WarningsAsErrors Condition="'$(TurboAIPolicyPhase)' == '2'"
>$(WarningsAsErrors);$(TurboAIWarningsAsErrors)</WarningsAsErrors
>
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Directory.Build.props XML is malformed around the conditional WarningsAsErrors property (opening/closing tags don't match and there's an extra standalone >). MSBuild will fail to parse this file. Rewrite it as a single well-formed element, e.g. <WarningsAsErrors Condition="'$(TurboAIPolicyPhase)' == '2'">...</WarningsAsErrors>.

Suggested change
<WarningsAsErrors Condition="'$(TurboAIPolicyPhase)' == '2'"
>$(WarningsAsErrors);$(TurboAIWarningsAsErrors)</WarningsAsErrors
>
<WarningsAsErrors Condition="'$(TurboAIPolicyPhase)' == '2'">$(WarningsAsErrors);$(TurboAIWarningsAsErrors)</WarningsAsErrors>

Copilot uses AI. Check for mistakes.
Comment on lines +166 to +170
if (isActivityPoints == true && activityPointType.HasValue && activityPointType.Value >= 0)
return activityPointType.Value;

return WalletCurrencyKeyMapper.TryGetActivityPointType(type, out var categoryId)
? categoryId
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResolveActivityPointCategory() falls back to parsing the raw type string even when CurrencyTypeEntity.IsActivityPoints is false. If a non-activity currency row uses a numeric type (e.g., "7" for credits after the backfill sets currency_type_id), this will incorrectly show up in ActivityPointsByCategoryId. Consider returning null when isActivityPoints == false, and only using WalletCurrencyKeyMapper parsing when the currency type link is missing/unknown or explicitly activity-points.

Copilot uses AI. Check for mistakes.
Comment on lines +202 to +205
finally
{
_reloadGate.Release();
}
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProcessReloadAsync() always calls _reloadGate.Release() in finally, but _reloadGate.WaitAsync(_cts.Token) can throw (e.g., during shutdown) before the semaphore is acquired. In that case Release() will throw SemaphoreFullException. Track whether the semaphore was acquired (or move the WaitAsync outside the try/finally) and only release when held.

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +67
migrationBuilder.AddForeignKey(
name: "FK_catalog_offers_currency_type_currency_type",
table: "catalog_offers",
column: "currency_type",
principalTable: "currency_type",
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This migration adds the FK from catalog_offers.currency_type to currency_type.id before seeding/backfilling rows into currency_type. If catalog_offers already has non-null currency_type values, adding the FK will fail immediately because the referenced ids don't exist yet. Seed/backfill currency_type (including ids referenced by existing offers) before adding the FK, or defer FK creation until after the data is populated.

Copilot uses AI. Check for mistakes.
Comment on lines +163 to +182
foreach (var request in requests)
{
if (string.IsNullOrWhiteSpace(request.CurrencyType) || request.Amount <= 0)
continue;

var canonicalType = WalletCurrencyKeyMapper.ToCanonicalKey(
request.CurrencyType,
request.CurrencyKind,
request.ActivityPointType
);
var key = (
CurrencyType: canonicalType,
Kind: request.CurrencyKind,
request.ActivityPointType,
request.CurrencyTypeId
);

if (!totals.TryAdd(key, request.Amount))
totals[key] += request.Amount;
}
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +76
foreach (var devPath in _config.DevPluginPaths)
{
var dir = Path.GetFullPath(devPath);

if (!Directory.Exists(dir))
continue;

try
{
var manifest = PluginHelpers.ReadManifest(dir);

if (manifest is not null)
if (seen.Add(manifest.Key))
{
list.Add((manifest, dir));
_logger.LogDebug("Discovered dev plugin {Key} from {Dir}", manifest.Key, dir);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to read plugin manifest in {Dir}", dir);
_logger.LogError(ex, "Failed to read dev plugin manifest in {Dir}", dir);
}
}
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.

Copilot uses AI. Check for mistakes.
@billsonnn billsonnn merged commit 096a036 into nitrodevco:main Feb 8, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants