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
43 changes: 39 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,48 @@ jobs:
path: artifacts/backend-tests
if-no-files-found: warn

# ─── Backend integration (deferredTestcontainers harness lights up P02c-2)
# ─── Backend integration (TestcontainersPostgres) ────────────────────
backend-integration:
name: backend integration (Testcontainers — deferred)
name: backend integration (Testcontainers)
runs-on: ubuntu-latest
if: false # activate when LearnStack.Hub.Tests.Integration has its first test
timeout-minutes: 25
steps:
- run: echo "Placeholder — P02c-2 wires the first Testcontainers integration test."
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Set up .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}

- name: Cache NuGet packages
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('backend/**/*.csproj', 'backend/Directory.Packages.props') }}
restore-keys: |
nuget-${{ runner.os }}-

- name: Restore
working-directory: backend
run: dotnet restore tests/LearnStack.Hub.Tests.Integration/LearnStack.Hub.Tests.Integration.csproj

- name: Test (integration — Testcontainers Postgres)
working-directory: backend
run: |
dotnet test tests/LearnStack.Hub.Tests.Integration/LearnStack.Hub.Tests.Integration.csproj \
--logger "trx;LogFileName=integration-results.trx" \
--results-directory ../artifacts/backend-integration-tests

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: backend-integration-test-results
path: artifacts/backend-integration-tests
if-no-files-found: warn

# ─── Frontend ───────────────────────────────────────────────────────────
frontend:
Expand Down
24 changes: 12 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ Hub **never** stores tenant content. Hub holds tenant _metadata_ (plan, subscrip

## What state this is in

**Phase 02c — Repository Bootstrap (P02c-0)** ✅. Solution scaffold + frontend monorepo + compose stack + CI + docs skeleton are in place. No Hub domain code yet — that lands in P02c-1 (Hub Domain Core).

| Packet | State |
| ---------------------------------------------------------------------------------------------------------------------------------------- | -------------- |
| P02c-0 — Repository bootstrap | ✅ this commit |
| P02c-1 — Hub Domain Core (`LearnStackTenant`, `Plan`, `HubSubscription`, `Entitlement`) | ⏳ next |
| P02c-2 — Hub-side internal API + outbound `LearnStackApiClient` | ⏳ |
| P02c-3 — LearnStack core PR (`HubEntitlementProvider`, `IUsageReporter`, internal-API handlers) — **blocked on LearnStack P02a-5/6/7/9** | ⏳ |
| P02c-4 — Operator portal MVP | ⏳ |
| P02c-5 — Custom domain lifecycle | ⏳ |
| P02c-6 — License key skeleton | ⏳ |
| P02c-7 — End-to-end exit gate | ⏳ |
**Phase 02c — Hub Domain Core (P02c-1)** ✅. The SharedKernel, the 6-step cross-cutting foundation, and the four domain modules (`TenantLifecycle`, `Plans`, `Subscriptions`, `Entitlements`) with their DbContexts, migrations, and the entitlement projection are in place. Next is P02c-2 (Hub-side internal API + outbound `LearnStackApiClient`).

| Packet | State |
| ---------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| P02c-0 — Repository bootstrap | ✅ |
| P02c-1 — Hub Domain Core (`LearnStackTenant`, `Plan`, `HubSubscription`, `Entitlement`) | |
| P02c-2 — Hub-side internal API + outbound `LearnStackApiClient` | ⏳ next |
| P02c-3 — LearnStack core PR (`HubEntitlementProvider`, `IUsageReporter`, internal-API handlers) — **blocked on LearnStack P02a-5/6/7/9** | ⏳ |
| P02c-4 — Operator portal MVP | ⏳ |
| P02c-5 — Custom domain lifecycle | ⏳ |
| P02c-6 — License key skeleton | ⏳ |
| P02c-7 — End-to-end exit gate | ⏳ |

## Where to start

Expand Down
5 changes: 5 additions & 0 deletions backend/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@ dotnet_diagnostic.CS8601.severity = error
dotnet_diagnostic.CS8602.severity = error
dotnet_diagnostic.CS8603.severity = error
dotnet_diagnostic.CS8604.severity = error

# EF Core migrations are tool-generated; treat them as generated code so the
# style analyzers (e.g. IDE0161 file-scoped namespace) do not gate the build.
[**/Migrations/*.cs]
generated_code = true
5 changes: 3 additions & 2 deletions backend/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@

<PropertyGroup Condition="'$(IsTestProject)' == 'true'">
<IsPackable>false</IsPackable>
<!-- xunit-pattern allowances; same as LearnStack core. -->
<NoWarn>$(NoWarn);CA1707;CA1812;CA1515;CA1034;CA2234</NoWarn>
<!-- xunit-pattern allowances; same as LearnStack core. CA1711: xUnit
collection-definition classes conventionally end in "Collection". -->
<NoWarn>$(NoWarn);CA1707;CA1812;CA1515;CA1034;CA2234;CA1711</NoWarn>
</PropertyGroup>

</Project>
40 changes: 35 additions & 5 deletions backend/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,47 @@
P02c-2 outbound HTTP client; P02c-5 cert provisioning). Declared centrally
so the version matrix is consistent the moment they are pulled in.
-->
<ItemGroup Label="Reserved for later P02c packets">
<ItemGroup Label="P02c-1 cross-cutting foundation (in use)">
<!-- Composition root + module registration. -->
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.0" />
<PackageVersion Include="FluentValidation.DependencyInjectionExtensions" Version="11.10.0" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.10.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.10.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.10.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.10.0" />

<!-- IProviderResilience<TPort> exposes the Polly v8 ResiliencePipeline. -->
<PackageVersion Include="Polly" Version="8.5.0" />

<!-- Serilog primary logger + console + OTLP sink (cross-cutting-foundation.md § 4). -->
<PackageVersion Include="Serilog" Version="4.1.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.OpenTelemetry" Version="4.1.1" />
<PackageVersion Include="Serilog.Formatting.Compact" Version="3.0.0" />

<!-- OpenTelemetry SDK: tracing + metrics, OTLP exporter (guarded by config endpoint).
1.15.x clears the NU1902 advisory (GHSA-4625-4j76-fww9) that flagged 1.10–1.12. -->
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
</ItemGroup>

<!--
EF Core OTel instrumentation (AddEntityFrameworkCoreInstrumentation) is
deferred: the only published package is a 1.x-beta whose OpenTelemetry.Api
floor conflicts with the stable 1.10.0 instrumentation set above and would
trip the CI version-conflict-as-error gate. Tracked as a P02c-2 follow-up
in docs/roadmap/README.md. Span coverage in P02c-1 comes from the ASP.NET
Core instrumentation + the manual learnstack.hub.* ActivitySources.
-->

<ItemGroup Label="Reserved for later P02c packets">
<PackageVersion Include="OpenTelemetry.Instrumentation.EntityFrameworkCore" Version="1.10.0-beta.1" />
</ItemGroup>

<!--
Expand Down
25 changes: 25 additions & 0 deletions backend/LearnStack.Hub.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,31 @@
</Folder>
<Folder Name="/src/Modules/">
<File Path="src/Modules/README.md" />
<File Path="src/Modules/Directory.Build.props" />
</Folder>
<Folder Name="/src/Modules/TenantLifecycle/">
<Project Path="src/Modules/TenantLifecycle/LearnStack.Hub.Modules.TenantLifecycle.Domain/LearnStack.Hub.Modules.TenantLifecycle.Domain.csproj" />
<Project Path="src/Modules/TenantLifecycle/LearnStack.Hub.Modules.TenantLifecycle.Application.Contracts/LearnStack.Hub.Modules.TenantLifecycle.Application.Contracts.csproj" />
<Project Path="src/Modules/TenantLifecycle/LearnStack.Hub.Modules.TenantLifecycle.Application/LearnStack.Hub.Modules.TenantLifecycle.Application.csproj" />
<Project Path="src/Modules/TenantLifecycle/LearnStack.Hub.Modules.TenantLifecycle.Infrastructure/LearnStack.Hub.Modules.TenantLifecycle.Infrastructure.csproj" />
</Folder>
<Folder Name="/src/Modules/Plans/">
<Project Path="src/Modules/Plans/LearnStack.Hub.Modules.Plans.Domain/LearnStack.Hub.Modules.Plans.Domain.csproj" />
<Project Path="src/Modules/Plans/LearnStack.Hub.Modules.Plans.Application.Contracts/LearnStack.Hub.Modules.Plans.Application.Contracts.csproj" />
<Project Path="src/Modules/Plans/LearnStack.Hub.Modules.Plans.Application/LearnStack.Hub.Modules.Plans.Application.csproj" />
<Project Path="src/Modules/Plans/LearnStack.Hub.Modules.Plans.Infrastructure/LearnStack.Hub.Modules.Plans.Infrastructure.csproj" />
</Folder>
<Folder Name="/src/Modules/Subscriptions/">
<Project Path="src/Modules/Subscriptions/LearnStack.Hub.Modules.Subscriptions.Domain/LearnStack.Hub.Modules.Subscriptions.Domain.csproj" />
<Project Path="src/Modules/Subscriptions/LearnStack.Hub.Modules.Subscriptions.Application.Contracts/LearnStack.Hub.Modules.Subscriptions.Application.Contracts.csproj" />
<Project Path="src/Modules/Subscriptions/LearnStack.Hub.Modules.Subscriptions.Application/LearnStack.Hub.Modules.Subscriptions.Application.csproj" />
<Project Path="src/Modules/Subscriptions/LearnStack.Hub.Modules.Subscriptions.Infrastructure/LearnStack.Hub.Modules.Subscriptions.Infrastructure.csproj" />
</Folder>
<Folder Name="/src/Modules/Entitlements/">
<Project Path="src/Modules/Entitlements/LearnStack.Hub.Modules.Entitlements.Domain/LearnStack.Hub.Modules.Entitlements.Domain.csproj" />
<Project Path="src/Modules/Entitlements/LearnStack.Hub.Modules.Entitlements.Application.Contracts/LearnStack.Hub.Modules.Entitlements.Application.Contracts.csproj" />
<Project Path="src/Modules/Entitlements/LearnStack.Hub.Modules.Entitlements.Application/LearnStack.Hub.Modules.Entitlements.Application.csproj" />
<Project Path="src/Modules/Entitlements/LearnStack.Hub.Modules.Entitlements.Infrastructure/LearnStack.Hub.Modules.Entitlements.Infrastructure.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/LearnStack.Hub.Tests.Unit/LearnStack.Hub.Tests.Unit.csproj" />
Expand Down
53 changes: 53 additions & 0 deletions backend/src/Core/LearnStack.Hub.Api/Common/HttpStatusMap.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Net;
using LearnStack.Hub.SharedKernel.Errors;
using LearnStack.Hub.SharedKernel.Results;

namespace LearnStack.Hub.Api.Common;

/// <summary>
/// Maps an <see cref="Error.Code"/> (or a <see cref="HubException"/> subclass)
/// to the HTTP status the API surface returns (mirror of LearnStack core's
/// <c>HttpStatusMap</c>). Adding a new error code requires updating the table.
/// </summary>
public static class HttpStatusMap
{
public static int For(Error error)
{
ArgumentNullException.ThrowIfNull(error);
return For(error.Code);
}

public static int For(string code) => code switch
{
"validation_failed" => (int)HttpStatusCode.BadRequest,
"unsupported_locale" => (int)HttpStatusCode.BadRequest,
"unauthorized" => (int)HttpStatusCode.Unauthorized,
"forbidden" => (int)HttpStatusCode.Forbidden,
"resource_scope_violation" => (int)HttpStatusCode.Forbidden,
"feature_disabled" => (int)HttpStatusCode.Forbidden,
"not_found" => (int)HttpStatusCode.NotFound,
"concurrency_conflict" => (int)HttpStatusCode.Conflict,
"business_rule_violation" => (int)HttpStatusCode.Conflict,
"rate_limited" => (int)HttpStatusCode.TooManyRequests,
"dependency_unavailable" => (int)HttpStatusCode.ServiceUnavailable,
_ => (int)HttpStatusCode.InternalServerError,
};

public static int For(Exception exception)
{
ArgumentNullException.ThrowIfNull(exception);

return exception switch
{
// 499 "client closed request" — the client disconnected before the
// response; the L1 handler skips both capture and body write for
// OperationCanceled. Status set here for parity with upstream proxies.
OperationCanceledException => 499,

// Every HubException carries a structured Error; deriving the status
// from Error.Code keeps body+status consistent.
HubException known => For(known.Error),
_ => (int)HttpStatusCode.InternalServerError,
};
}
}
Loading
Loading