diff --git a/.gitignore b/.gitignore index 591f80e..de0bceb 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,6 @@ source/node_modules # Reverts !build/ -!build/GetBuildVersion.psm1 \ No newline at end of file +!build/GetBuildVersion.psm1 + +.ai-context/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a23e823 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,189 @@ +--- +title: WebSmsNet - Claude Code Instructions +tags: [claude, onboarding, dotnet, api-client, sms, linkmobility, websms] +--- + +# WebSmsNet + +Unofficial .NET client library for the [LINK Mobility websms Messaging REST API 1.0.0](https://developer.linkmobility.eu/sms-api/rest-api). The library provides a typed C# client for sending text and binary SMS, parsing inbound webhook notifications (text, binary, delivery report), and easy registration through ASP.NET Core dependency injection. It ships as three NuGet packages: `WebSmsNet`, `WebSmsNet.Abstractions`, and `WebSmsNet.AspNetCore`. + +> **AI agents:** strict procedural rules live in [`ai_instructions.md`](./ai_instructions.md). That file is the contract for agent behavior and overrides this file where they disagree. + +## Tech Stack + +| Layer | Technology | +| ---------------- | ---------------------------------------------------------------------------- | +| Language | C# 13 (`13`) | +| Framework | .NET 9 (`net9.0`) | +| HTTP | `System.Net.Http.HttpClient` via `IHttpClientFactory` (ASP.NET Core package) | +| Serialization | `System.Text.Json` (web defaults + camel-case enum converter) | +| DI integration | ASP.NET Core (`Microsoft.Extensions.DependencyInjection`, `Microsoft.Extensions.Http`) | +| Testing | xUnit 2.9 + FluentAssertions 6.12 + Coverlet (`WebSmsNet.Tests`) | +| Build | MSBuild (SDK-style csproj) — `GeneratePackageOnBuild` + `GenerateDocumentationFile` | +| CI/CD | GitHub Actions (`main.yml`, `codeql-analysis.yml`, `sonar-analysis.yml`) | +| License | MIT | + +> Note: the testing stack (xUnit + FluentAssertions) differs from the AMANDA-Technology org default (NUnit + Shouldly). Match the **existing** project style when adding tests here — do not migrate the test framework as a side-effect of another change. + +## Solution Structure + +``` +WebSmsNet.sln + src/ + WebSmsNet.Abstractions/ -- Models, enums, serializers, DI-agnostic helpers (NuGet package) + Configuration/ -- WebSmsApiOptions, AuthenticationType, HttpClient extension + Connectors/ -- Connector interfaces (IMessagingConnector) + Helpers/ -- BinaryContent helper (UDH-aware Base64 splitter/joiner) + Models/ -- Request/response DTOs and webhook models + Enums/ -- Public enums (WebSmsStatusCode, AddressType, MessageType, ...) + Serialization/ -- WebSmsJsonSerialization, WebSmsWebhookRequestConverter + IWebSmsApiClient.cs -- Aggregate client contract + WebSmsNet/ -- Core implementation (NuGet package) + Connectors/ -- Connector implementations (MessagingConnector) + WebSmsApiClient.cs -- Aggregate client, wires connectors to the connection handler + WebSmsApiConnectionHandler.cs -- HTTP POST dispatcher with virtual extension points + WebSmsNet.AspNetCore/ -- DI integration (NuGet package) + Configuration/ -- AddWebSmsApiClient(IServiceCollection, ...) extension + Helpers/ -- WebSmsWebhook parsing/matching helpers for webhook endpoints + tests/ + WebSmsNet.Tests/ -- xUnit tests (DI, serialization, webhook parse, live send when env set) + build/ + GetBuildVersion.psm1 -- PowerShell version helper used by the release workflow + .github/ + workflows/ -- main.yml (build/publish), codeql-analysis.yml, sonar-analysis.yml + dependabot.yml +``` + +### Dependency Graph + +``` +WebSmsNet.Tests --> WebSmsNet.AspNetCore --> WebSmsNet --> WebSmsNet.Abstractions +``` + +## Build Commands + +```bash +# Restore, build, test the whole solution +dotnet restore WebSmsNet.sln +dotnet build WebSmsNet.sln +dotnet build WebSmsNet.sln -c Release # also produces .nupkg (GeneratePackageOnBuild) +dotnet test WebSmsNet.sln + +# Run just the tests (env vars required for the live-send tests — see below) +dotnet test tests/WebSmsNet.Tests/WebSmsNet.Tests.csproj + +# Pack explicitly +dotnet pack WebSmsNet.sln -c Release +``` + +### Env vars for live integration tests + +The current `MessagingTests` fixture constructs a real `WebSmsApiClient` against `https://api.linkmobility.eu/` and reads credentials / recipient from the environment. Tests that hit the API will fail without these; pure serialization / parsing tests do not need them: + +| Variable | Purpose | +| ------------------------------- | ------------------------------------------------- | +| `Websms_AccessToken` | Bearer token used by `SendTextMessage` / `SendBinaryMessage` | +| `Websms_RecipientAddressList` | A test recipient MSISDN (E.164, single entry) | + +## Key Conventions + +### Architecture Pattern +- **Aggregate client + connectors.** `IWebSmsApiClient` is the single entry point and exposes one property per API area. Today only `Messaging` (`IMessagingConnector`) exists; future endpoint groups add new connector properties alongside it. +- **Connection handler in the middle.** `WebSmsApiConnectionHandler` wraps a single `HttpClient`, owns JSON serialization, and is the only thing that talks HTTP. Connectors call `connectionHandler.Post(endpoint, data, cancellationToken)` — they do not touch `HttpClient` directly. +- **Three construction paths, one handler contract.** `WebSmsApiClient` has constructors for `WebSmsApiOptions`, `HttpClient`, and `WebSmsApiConnectionHandler`. The DI path resolves the handler through `AddHttpClient` (typed client). +- **Extensible via virtuals.** `WebSmsApiConnectionHandler` exposes `SerializerOptions`, `OnBeforePost`, `OnResponseReceived`, `EnsureSuccess`, and virtual `Post`. Consumers derive a custom handler to add auditing, retries, custom error mapping, etc. — see the README "Custom connection handler" section. +- **Endpoint path constants live next to the connector.** `MessagingConnector.MessagingApiBasePath = "/rest/smsmessaging"` — do not inline endpoint strings scattered across the codebase. + +### Code Style +- **Sealed records for DTOs and responses.** `MessageSendResponse` is a `sealed record` with `init` properties and `required`. New response DTOs should follow the same shape. +- **Classes for request bodies.** `SmsSendRequest`, `TextSmsSendRequest`, `BinarySmsSendRequest` are classes today (with a mutable, settable base) — this preserves the existing public API. New mutable request shapes may stay as classes; new immutable ones should be records. +- **Nullable reference types enabled** (`enable`) in every csproj. +- **XML doc comments on every public / protected member.** All shipping projects set `true` — missing doc comments produce build warnings. +- **`[JsonPropertyName]` on every serialized property.** Names match the websms JSON exactly (camelCase, including quirks like `validityPeriode`). +- **`[SuppressMessage("ReSharper", "...")]`** is used liberally for R# noise on public API types (e.g., `UnusedMember.Global`, `ClassNeverInstantiated.Global`). Match the existing style. +- **`[Optional] CancellationToken`** is used across public async methods — keep the convention. + +### Serialization +- `System.Text.Json` with `JsonSerializerDefaults.Web`, `DefaultIgnoreCondition = WhenWritingNull`, `WriteIndented = false`. +- Enums serialize as **camelCase strings** via `JsonStringEnumConverter(JsonNamingPolicy.CamelCase)` — except `WebSmsStatusCode`, which is serialized as its **integer value** via `[JsonConverter(typeof(JsonNumberEnumConverter))]` on each response property. +- Inbound webhook payloads discriminate on the `messageType` string (`"text"`, `"binary"`, `"deliveryReport"`). The `WebSmsWebhookRequestConverter` reads the discriminator and re-deserializes into the matching subtype of `WebSmsWebhookRequest.Base`. +- Both the converter and the enum converter are pre-registered in `WebSmsJsonSerialization.DefaultOptions`. Use these options everywhere (including webhook parsing) so inbound and outbound behavior stay consistent. + +### Authentication +- Two modes, selected by `WebSmsApiOptions.AuthenticationType`: + - `Bearer` — requires `AccessToken`, sent as `Authorization: Bearer `. + - `Basic` — requires `Username` + `Password`, sent as `Authorization: Basic `. +- The `HttpClient` is configured once by `HttpClientExtension.ApplyWebSmsApiOptions`. No per-request header mutation. +- Missing credentials throw `InvalidOperationException` at client construction / first use. There is no silent fallback. + +### Webhook Handling +- Consumers receive three shapes from the websms platform: `WebSmsWebhookRequest.Text`, `.Binary`, `.DeliveryReport`, all deriving from `WebSmsWebhookRequest.Base`. +- Parse with `WebSmsWebhook.Parse(string json)` or `WebSmsWebhook.Parse(Stream requestStream, ...)`. Dispatch with `Match(onText, onBinary, onDeliveryReport)` — both an `Action` and a `Func` overload exist. +- Respond with `WebSmsWebhook.CreateOkResponse()` / `CreateErrorResponse(string message)`. The helper uses `WebSmsStatusCode.Ok` / `InternalError` and a status message. +- Binary payloads are Base64-encoded segments optionally prefixed by a UDH; `BinaryContent.Parse` / `BinaryContent.CreateMessageContentParts` split and reassemble concatenated SMS segments. + +### Testing Pattern (current state) +- **Framework:** xUnit 2.9 + FluentAssertions 6.12 + Coverlet. `[Fact]` for individual tests. +- **Locations:** a single test project (`tests/WebSmsNet.Tests`). Tests cover DI wiring, serialization/deserialization of `MessageSendResponse`, webhook parsing, `BinaryContent` round-trip, and live send (requires env vars). +- **Live-hit tests:** construct the client in a field initializer that reads `Websms_AccessToken` / `Websms_RecipientAddressList` from the environment and throws if absent. Do not hardcode credentials. +- **Assertions:** FluentAssertions (`result.Should().Be(...)`). Match existing style when adding tests — do not mix in NUnit or Shouldly. + +## Important File Locations + +| Purpose | Path | +| ------------------------------ | -------------------------------------------------------------------------- | +| AI rules | `ai_instructions.md` | +| Main client interface | `src/WebSmsNet.Abstractions/IWebSmsApiClient.cs` | +| Client implementation | `src/WebSmsNet/WebSmsApiClient.cs` | +| HTTP connection handler | `src/WebSmsNet/WebSmsApiConnectionHandler.cs` | +| Messaging connector (interface)| `src/WebSmsNet.Abstractions/Connectors/IMessagingConnector.cs` | +| Messaging connector (impl) | `src/WebSmsNet/Connectors/MessagingConnector.cs` | +| API options | `src/WebSmsNet.Abstractions/Configuration/WebSmsApiOptions.cs` | +| HttpClient configuration | `src/WebSmsNet.Abstractions/Configuration/HttpClientExtension.cs` | +| Authentication types | `src/WebSmsNet.Abstractions/Configuration/AuthenticationType.cs` | +| DI registration | `src/WebSmsNet.AspNetCore/Configuration/WebSmsApiServiceCollectionExtension.cs` | +| Webhook helpers | `src/WebSmsNet.AspNetCore/Helpers/WebSmsWebhook.cs` | +| Binary content helper | `src/WebSmsNet.Abstractions/Helpers/BinaryContent.cs` | +| JSON defaults | `src/WebSmsNet.Abstractions/Serialization/WebSmsJsonSerialization.cs` | +| Webhook polymorphic converter | `src/WebSmsNet.Abstractions/Serialization/WebSmsWebhookRequestConverter.cs`| +| Request DTOs | `src/WebSmsNet.Abstractions/Models/*SmsSendRequest.cs` | +| Response DTOs | `src/WebSmsNet.Abstractions/Models/MessageSendResponse.cs` | +| Webhook DTOs | `src/WebSmsNet.Abstractions/Models/WebSmsWebhookRequest.cs`, `WebSmsWebhookResponse.cs` | +| Enums | `src/WebSmsNet.Abstractions/Models/Enums/` | +| Tests | `tests/WebSmsNet.Tests/MessagingTests.cs` | +| CI build & publish | `.github/workflows/main.yml` | +| CodeQL | `.github/workflows/codeql-analysis.yml` | +| SonarCloud | `.github/workflows/sonar-analysis.yml` | +| Release version helper | `build/GetBuildVersion.psm1` | + +## Known Constraints and Gotchas + +1. **Live tests need real credentials.** `MessagingTests.SendTextMessage` / `SendBinaryMessage` construct a `WebSmsApiClient` in a field initializer and will throw if `Websms_AccessToken` / `Websms_RecipientAddressList` are missing — the *whole test class* cannot instantiate. When running tests locally or in CI without credentials, expect these to fail at test discovery/setup. Parse / serialize / DI tests do not need env vars but live alongside the failing-constructor fixture. +2. **`WebSmsStatusCode` is an integer on the wire.** The enum's members use explicit websms codes (e.g., `Ok = 2000`). Serialize via `JsonNumberEnumConverter` — never as a camelCase string, or the API will reject the payload. +3. **Webhook discriminator is `messageType`.** `WebSmsWebhookRequestConverter` dispatches on `"text"`, `"binary"`, `"deliveryReport"`; any other value throws `JsonException`. If websms ever adds a new webhook type, extend the switch there and add a sibling type under `WebSmsWebhookRequest`. +4. **`WebSmsApiConnectionHandler` owns its `HttpClient` lifecycle only when constructed via `WebSmsApiOptions`.** In the DI path, `IHttpClientFactory` owns the socket. Do not dispose the handler-held client in derived classes. +5. **POST-only API surface.** The handler currently only exposes `Post(...)`. If a future endpoint needs GET/PUT/DELETE, extend the handler with matching virtual methods — do not instantiate `HttpClient` from a connector. +6. **`GeneratePackageOnBuild` is on for all three library projects.** Every `dotnet build` produces `.nupkg` files in `bin//`. `.gitignore` excludes `bin/` and `obj/`. +7. **Target framework is `net9.0`, LangVersion 13.** Do not mix `net10.0` / C# 14 language features into this repo without an explicit framework bump — other AMANDA-Technology libraries (BexioApiNet, CashCtrlApiNet) target `net10.0`, this one currently does not. +8. **`validityPeriode` (sic).** The property name on the wire is misspelled by the websms API. `SmsSendRequest.ValidityPeriod` maps to `[JsonPropertyName("validityPeriode")]` — keep the misspelling on the JSON side. +9. **No pagination, search, bulk, or binary-file endpoints.** Unlike BexioApiNet / CashCtrlApiNet, the websms Messaging API is a single POST-per-call surface. Do not import the `FetchAll` / `SearchCriteria` / `PostBulkAsync` abstractions from those libraries. + +## Related AMANDA-Technology Repositories + +These are sibling .NET API-client libraries by the same org and are useful as style references, though their tech stacks may differ (`net10.0`, NUnit + Shouldly, different API shape): + +- [BexioApiNet](https://github.com/AMANDA-Technology/BexioApiNet) — Bexio REST API v3.0.0 (accounting/banking). Has both `CLAUDE.md` and `ai_instructions.md`; its connector + `ConnectionHandler` + DI layout directly inspired this one. +- [CashCtrlApiNet](https://github.com/AMANDA-Technology/CashCtrlApiNet) — CashCtrl REST API v1 (Swiss cloud ERP). Connector-per-domain pattern, form-encoded bodies, similar three-package split. + +When taking inspiration from those repos, copy the *patterns* (connector/handler split, DI registration shape, XML doc discipline) rather than the *tech* (framework version, testing libraries) — this project is older and has its own stack as documented above. + +## AI Agent Workflow (Quick Reference) + +For full rules, see [`ai_instructions.md`](./ai_instructions.md). At a glance: + +- **Personas:** documentation/onboarding → *developer* (C# library). Infrastructure-only issues (workflows, Dockerfiles, scripts) → *devops*. Architecture blueprints / cross-repo specs → *architect*. +- **Issue complexity:** `trivial` (one-file, one-line fix), `simple` (single feature / doc), `complex` (multi-file feature), `epic` (multi-phase initiative). +- **Scope discipline.** Make the minimum change that fulfills the task. Do not refactor unrelated code or migrate the test framework, target framework, or language version as a side effect. +- **Build before finishing.** Run `dotnet build` (0 errors, 0 warnings — XML doc warnings count) and `dotnet test` where feasible. +- **Commit before finishing.** Every agent run ends with either a clean tree or a new commit. Never leave staged/unstaged diffs behind. +- **Secrets.** Never write tokens, recipient MSISDNs, or customer data into source, tests, fixtures, or docs. Reference env vars by name only. diff --git a/ai_instructions.md b/ai_instructions.md new file mode 100644 index 0000000..02383c6 --- /dev/null +++ b/ai_instructions.md @@ -0,0 +1,186 @@ +--- +title: AI Agent Instructions for WebSmsNet +tags: [ai, instructions, contributing, conventions] +--- + +# AI Agent Instructions — WebSmsNet + +This file contains **strict, procedural instructions** for AI coding agents working in this repository. Follow these rules to the letter — they keep the library production-grade and aligned with the LINK Mobility websms Messaging REST API. + +- This file: **strict rules for AI agents** (what to do, what never to do). +- [`CLAUDE.md`](./CLAUDE.md): **high-level overview** (tech stack, structure, file locations, gotchas). +- [`README.md`](./README.md): **end-user documentation** (installation, usage, custom handler). + +If anything here conflicts with `CLAUDE.md`, **this file wins for agent behavior**. + +## 1. Mission & Source of Truth + +1. This library is a typed .NET client for the **LINK Mobility websms Messaging REST API 1.0.0**. Source of truth: . +2. Every endpoint, DTO field, enum value, status code and query / body parameter must match the websms docs exactly. If the docs and the code disagree, **the docs are right** — open a change. +3. Never invent endpoints, fields or behavior that the websms docs do not describe. If the docs are ambiguous, stop and ask rather than guessing. +4. The API's JSON is snake-ish camelCase with a handful of quirks (e.g., `validityPeriode` is misspelled on the wire). Preserve those on the JSON side via `[JsonPropertyName]`; the C# name should be the correctly-spelled `ValidityPeriod`. + +## 2. Tech Stack (non-negotiable) + +| Concern | Value | +| -------------- | ------------------------------------------------------------------ | +| Language | C# 13 (`13`) | +| Framework | .NET 9 (`net9.0`) — all three library projects and the test project | +| JSON | `System.Text.Json` — **never** add Newtonsoft.Json | +| DI | `Microsoft.Extensions.DependencyInjection` + `Microsoft.Extensions.Http` (typed clients) | +| Tests | xUnit 2.9 + FluentAssertions 6.12 + Coverlet | +| Nullability | `enable` everywhere | +| XML docs | `true` on all three shipping projects | +| Packaging | `true` — build produces `.nupkg` for each library | + +If a change would require bumping any of these (e.g., moving to `net10.0` or swapping xUnit for NUnit), **stop and escalate**. This project intentionally lags its siblings (BexioApiNet, CashCtrlApiNet) and must not be migrated silently. + +## 3. Architecture Patterns (do not deviate) + +### 3.1 Aggregate client + connectors +- `IWebSmsApiClient` (in `WebSmsNet.Abstractions`) is the aggregate root. It exposes one property per API area. +- Today the only area is `Messaging` (`IMessagingConnector`). Adding a new API area means: + 1. Add a new connector interface under `src/WebSmsNet.Abstractions/Connectors/IConnector.cs`. + 2. Add the implementation under `src/WebSmsNet/Connectors/Connector.cs`, constructor-injecting `WebSmsApiConnectionHandler`. + 3. Add the property to `IWebSmsApiClient` and assign it in `WebSmsApiClient`. + 4. No new DI registration is needed — connectors are wired directly by `WebSmsApiClient`. `AddWebSmsApiClient` only registers the handler and the client. + +### 3.2 Single `WebSmsApiConnectionHandler` for HTTP +- All HTTP access goes through `WebSmsApiConnectionHandler`. Connectors **never** new up `HttpClient` or call it directly. +- The handler today exposes one public verb, `Task Post(string endpoint, object data, CancellationToken)`. Endpoints that need other verbs must be added as sibling virtual methods on the handler (e.g., `Get`, `Delete`, `GetBinary`) — not inline in connectors. +- The handler is designed to be subclassed. The `SerializerOptions`, `OnBeforePost`, `OnResponseReceived`, `EnsureSuccess`, and `Post` members are all `virtual` / `protected virtual`. Do not tighten these to `private` / `sealed` — the README documents deriving from the handler as the public extension point. +- Three construction paths exist, all ending in the same handler: + - `WebSmsApiClient(WebSmsApiOptions)` — owns a new `HttpClient` (non-DI). + - `WebSmsApiClient(HttpClient)` — caller supplies the client. + - `WebSmsApiClient(WebSmsApiConnectionHandler)` — caller supplies a fully-custom handler. + Keep all three working when touching the client. + +### 3.3 DI registration +- `AddWebSmsApiClient(IServiceCollection, Action)`: + 1. `services.Configure(configureOptions)`. + 2. `services.AddHttpClient(...)` — typed client; reads `IOptions` inside the configure delegate and calls `ApplyWebSmsApiOptions`. + 3. `services.AddScoped(...)` — resolves `WebSmsApiConnectionHandler` from the provider and constructs `WebSmsApiClient`. +- Do **not** replace this with `services.AddScoped(...)` — that defeats `IHttpClientFactory` and brings back the classic socket-exhaustion bug. +- Do **not** change the scope from `Scoped` to `Singleton` without verifying `HttpClient` lifetime semantics — typed clients are scoped by default. + +### 3.4 Endpoint paths as constants +- Paths live in a `private const string` on the connector (e.g., `MessagingConnector.MessagingApiBasePath = "/rest/smsmessaging"`). Endpoint *variants* (`"/text"`, `"/binary"`) are concatenated at call time. +- Do not inline literal path strings outside the connector. If a group of related endpoints grows beyond a few, promote the paths to a static sibling class (mirroring the `*Configuration` / `Endpoints` patterns in BexioApiNet / CashCtrlApiNet). + +### 3.5 Request and response shapes +- **Request bodies** inherit from `SmsSendRequest` (an abstract class with `required` properties + `get; set;`). `TextSmsSendRequest` and `BinarySmsSendRequest` are `sealed`. Keep the abstract base mutable — consumers compose requests property-by-property. +- **Response bodies** are `sealed record` with `required ... { get; init; }`. Match this for any new response DTO. +- Every serialized property has `[JsonPropertyName("...")]` matching the websms wire name exactly (camelCase, including quirks). Do not rely on `JsonSerializerDefaults.Web` name inference. +- Nullable → nullable. If websms can return / accept `null`, the property type is nullable (`string?`, `int?`, `ContentCategory?`). + +### 3.6 Enums +- Live in `src/WebSmsNet.Abstractions/Models/Enums/`. Each value has an XML `` with the human-readable meaning. +- Default enum serialization is **camelCase string** via the global `JsonStringEnumConverter(JsonNamingPolicy.CamelCase)` in `WebSmsJsonSerialization.DefaultOptions`. +- **Exception:** `WebSmsStatusCode` serializes as its **integer** websms code (e.g., `2000`). It is the only enum whose on-the-wire form is numeric. Apply this on each property that uses it: + ```csharp + [JsonPropertyName("statusCode")] + [JsonConverter(typeof(JsonNumberEnumConverter))] + public required WebSmsStatusCode StatusCode { get; init; } + ``` + When adding a new response type that includes a status code, repeat this attribute pair. Do not "simplify" by removing either attribute. + +### 3.7 Webhook polymorphism +- `WebSmsWebhookRequest` is a **static container class** with nested `Base`, `TextAndBinaryBase`, `Text`, `Binary`, `DeliveryReport`. The polymorphism is read through `WebSmsWebhookRequestConverter`, which dispatches on `messageType`. +- Adding a new webhook kind means: + 1. Add a nested class under `WebSmsWebhookRequest` inheriting from `Base` (or `TextAndBinaryBase`). + 2. Add a case to `WebSmsWebhookRequestConverter.Read` for the new discriminator value. + 3. Add a value to the `WebhookMessageType` enum. + 4. Extend `WebSmsWebhook.Match(...)` with an additional `onXxx` parameter — **breaking API change**, call it out. +- Inbound webhook parsing always uses `WebSmsJsonSerialization.DefaultOptions`. Do not parse webhook JSON with ad-hoc options. + +### 3.8 Extension points on the handler +- `OnBeforePost(endpoint, data, cancellationToken)` — auditing / logging / pre-flight mutation. +- `OnResponseReceived(response, cancellationToken)` — runs **before** `EnsureSuccess`; returns the response (possibly replaced) for chaining. +- `EnsureSuccess(response)` — defaults to `response.EnsureSuccessStatusCode()`. Override to translate websms error bodies into richer exceptions. +- `SerializerOptions` — defaults to `WebSmsJsonSerialization.DefaultOptions`. Override to add custom converters or change casing. +- When extending the handler, **preserve this order**: `OnBeforePost` → HTTP call → `OnResponseReceived` → `EnsureSuccess` → `ReadFromJsonAsync`. Tests and consumers rely on it. + +## 4. Model & DTO Rules + +1. **Records for responses**, **classes for mutable requests** — match what exists today. Do not silently convert a class to a record (it changes equality and breaks consumers). +2. Use `required` for properties that websms always sends back or always needs. Use `init` for immutable properties on records; `get; set;` on request classes. +3. **Enable nullable reference types.** If websms can return `null`, the property type must be nullable. +4. Map property names with `[JsonPropertyName("...")]` that match websms exactly, including any misspellings on the websms side. +5. Place new DTOs under `src/WebSmsNet.Abstractions/Models//` when an area grows beyond the current flat layout. Enums go in `Models/Enums/`. +6. Every `public` / `protected` type and member needs an XML ``. Missing docs produce build warnings, which we treat as errors. +7. `[SuppressMessage("ReSharper", "...")]` is used liberally on public API surface types to silence ReSharper's "unused" noise. Match the existing pattern rather than disabling R# project-wide. + +## 5. Serialization Rules + +1. Go through `WebSmsJsonSerialization.DefaultOptions` for all serialization / deserialization — outbound bodies via the handler, inbound webhooks via `WebSmsWebhook.Parse`. +2. Do not mutate `DefaultOptions` at call sites except for local diagnostic formatting (tests may set `WriteIndented = true` on a local copy). +3. If a new converter is needed, add it to the `Converters` collection in `WebSmsJsonSerialization.DefaultOptions` and keep a sibling `JsonConverter` class in `src/WebSmsNet.Abstractions/Serialization/`. +4. Never write a Newtonsoft.Json converter. Never pull `Newtonsoft.Json` into the solution. + +## 6. Testing Rules + +1. **Framework:** xUnit 2.9 + FluentAssertions 6.12. Use `[Fact]` for individual tests. Do not introduce `[Theory]` where a `[Fact]` suffices. +2. **Do not migrate the testing stack.** NUnit, Shouldly, NSubstitute, Bogus, WireMock.Net are used by sibling libraries (BexioApiNet, CashCtrlApiNet) — they are **not** adopted here. A test-framework migration is its own PR, not a side-effect. +3. **New connector methods require a test.** At minimum: a DI-wiring test verifying the method is reachable through `IWebSmsApiClient`, and a serialization round-trip for the request / response DTOs involved. +4. **Live-send tests** use env vars (`Websms_AccessToken`, `Websms_RecipientAddressList`) and **must throw** at setup when the vars are absent, matching the existing `MessagingTests` pattern. Do not silently `Skip` tests or add a "local dev" fallback that hardcodes credentials. +5. **No mocked handler frameworks.** For isolation, construct a fake `WebSmsApiConnectionHandler` subclass in the test project — the handler's virtual members make this straightforward. Do not pull in NSubstitute / Moq. +6. Do **not** add test-only code paths to production types. If a test needs an override, derive from the handler in the test project. +7. Never commit real tokens, recipient MSISDNs, or customer data in test fixtures. Read them from environment variables. + +## 7. Build & Verification Rules + +1. Before completing any task, run `dotnet build WebSmsNet.sln` and confirm **0 errors and 0 warnings**. XML-doc warnings count as errors for this project — all three shipping projects have `GenerateDocumentationFile` on. +2. Run `dotnet test` where feasible. The live-send fixture will fail without env vars — that is expected locally. Verify the parse/serialize/DI tests still pass. +3. Never introduce `.Result` or `.Wait()` on tasks. Use `async` / `await` end-to-end. The handler's `Post` is already fully async. +4. Keep `ImplicitUsings` and `Nullable` enabled in every csproj. Do not disable them per-file. +5. Do not commit artifacts (`bin/`, `obj/`, `*.nupkg`). They are in `.gitignore`. + +## 8. Secrets & Security + +1. Never write real websms tokens, basic-auth passwords, customer MSISDNs, or webhook bodies containing real phone numbers into source, tests, fixtures, or docs. +2. `Websms_AccessToken` and `Websms_RecipientAddressList` are supplied via the environment only. Reference them by name — never by value. +3. Never log or serialize `Authorization` headers, `AccessToken`, or `Password` values. If adding logging in a custom handler, redact these fields. +4. Do not commit `.env`, `appsettings.Development.json`, or any file containing credentials. + +## 9. Workflow Expectations for AI Agents + +1. **Scope discipline.** Apply the minimum change that fulfills the task. Do not refactor unrelated code "on the way through". +2. **No speculative features.** Implement only what the issue / blueprint requests. Hypothetical future needs are not a reason to add abstractions. +3. **No silent framework / library migrations.** Target framework, C# language version, and testing stack are all explicit in this document. Changing any of them is its own ticket. +4. **Incremental verification.** Build after each significant change, not only at the end. +5. **Commit before finishing.** Every agent run ends with either a clean tree or a new commit — never staged/unstaged diffs. +6. **Update docs alongside code.** If a convention changes, update this file, `CLAUDE.md`, and `README.md` (if user-facing) in the same change. +7. If the websms docs describe something this library does not yet support, do not silently stub it. Either implement it end-to-end or open a tracked backlog item. + +## 10. Forbidden Patterns + +Do not: +- Instantiate `HttpClient` directly in new code. Always route through the injected / constructed `WebSmsApiConnectionHandler`. +- Throw bare exceptions from connector methods for ordinary API failures — rely on `EnsureSuccess` (and its override points) so consumers can plug in custom error translation. +- Convert `sealed record` response types to `class` or vice-versa without explicit need — both changes are breaking. +- Add Newtonsoft.Json, AutoMapper, MediatR, NUnit, Shouldly, NSubstitute, Moq, or any framework not already on the dependency list. +- Skip XML doc comments on public members — they are compiled into the NuGet packages and missing docs break the build. +- Hardcode endpoint paths in multiple places — use the `const string` pattern on the connector. +- Silently change the `WebSmsStatusCode` serialization from integer to string, or drop `[JsonNumberEnumConverter]` from a response property. +- Bump `` from `net9.0` to `net10.0` or `` beyond 13 as a side-effect. That is an explicit, standalone change. +- Rename `ValidityPeriod` → `ValidityPeriode` to "fix" the C# property name. The correctly-spelled C# name with the wire-misspelled `[JsonPropertyName]` is intentional. + +## 11. Persona Routing + +When an issue arrives, choose the persona that matches the work: + +- **developer** — C# / .NET implementation, bug fixes, new connector methods, serialization work, test authoring. This is the default for anything under `src/` or `tests/`. +- **devops** — `.github/workflows/*.yml`, `build/*.psm1`, Dockerfiles, dependabot config, release packaging changes. Not subject to TDD. +- **architect** — cross-repo blueprints, large feature specs, breaking-API decisions. Produces design docs, not code. +- **verification** — reviews a feature branch's diff + build + tests before merge. Does not author feature code. + +Onboarding / documentation tasks (this issue) are handled by *developer* with a docs-only diff. + +## 12. When in Doubt + +1. Re-read the relevant section of . +2. Compare with the fully-implemented example in this repo: `MessagingConnector` + `TextSmsSendRequest` + `MessageSendResponse` + `MessagingTests` is the canonical reference. +3. Cross-check against the sibling libraries for *patterns only* — do not copy their tech choices: + - [BexioApiNet](https://github.com/AMANDA-Technology/BexioApiNet) — connector + handler + DI patterns, `ai_instructions.md` template. + - [CashCtrlApiNet](https://github.com/AMANDA-Technology/CashCtrlApiNet) — three-package split, connector-per-domain shape. +4. If still unclear, stop and surface the question in the task result rather than guessing.