Catalog wallet multi-currency + currency_type linkage#7
Conversation
- add canonical AGENTS/CONTEXT/copilot/PR template guidance for AI-assisted work\n- enforce quality gates and required governance artifacts via build targets and hooks\n- add boost-style prompt contract, task recipes, and docs/patterns references
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…g-base-with-guardrails-autoreload
There was a problem hiding this comment.
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_typemodeling + FK linkage/backfill forplayer_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.
| if (seen.Add(manifest.Key)) | ||
| { | ||
| list.Add((manifest, dir)); | ||
| _logger.LogDebug("Discovered dev plugin {Key} from {Dir}", manifest.Key, dir); | ||
| } |
There was a problem hiding this comment.
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.
| <WarningsAsErrors Condition="'$(TurboAIPolicyPhase)' == '2'" | ||
| >$(WarningsAsErrors);$(TurboAIWarningsAsErrors)</WarningsAsErrors | ||
| > |
There was a problem hiding this comment.
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>.
| <WarningsAsErrors Condition="'$(TurboAIPolicyPhase)' == '2'" | |
| >$(WarningsAsErrors);$(TurboAIWarningsAsErrors)</WarningsAsErrors | |
| > | |
| <WarningsAsErrors Condition="'$(TurboAIPolicyPhase)' == '2'">$(WarningsAsErrors);$(TurboAIWarningsAsErrors)</WarningsAsErrors> |
| if (isActivityPoints == true && activityPointType.HasValue && activityPointType.Value >= 0) | ||
| return activityPointType.Value; | ||
|
|
||
| return WalletCurrencyKeyMapper.TryGetActivityPointType(type, out var categoryId) | ||
| ? categoryId |
There was a problem hiding this comment.
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.
| finally | ||
| { | ||
| _reloadGate.Release(); | ||
| } |
There was a problem hiding this comment.
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.
| migrationBuilder.AddForeignKey( | ||
| name: "FK_catalog_offers_currency_type_currency_type", | ||
| table: "catalog_offers", | ||
| column: "currency_type", | ||
| principalTable: "currency_type", |
There was a problem hiding this comment.
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.
| 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; | ||
| } |
There was a problem hiding this comment.
This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.
| 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.
Summary
currency_typelookup + protocol-id based currency modelingplayer_currenciestocurrency_type(currency_type_idFK + backfill)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:TurboCloudFastCheckdotnet build Turbo.Main/Turbo.Main.csproj -t:TurboCloudQualityGate