TDD: IEmailService interface, EmailService resolves member emails + raw addresses (case-insensitive dedup), sends via ISmtpDispatcher, writes a NotificationLog per recipient (sent/failed), and never aborts the batch on a single failure.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements LineSignature.IsValid() using HMAC-SHA256 + FixedTimeEquals to prevent timing attacks; includes xUnit tests for valid, tampered, and null/empty header cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Live verification revealed the JWT carries the user id in the 'sub' claim
(NameClaimType=sub, MapInboundClaims=false), so ClaimTypes.NameIdentifier is
null at runtime. This caused ExpensesController.GetMine/GetById to throw
NullReferenceException (500) on the '!.Value', and made the services fall back
to 'system' — silently defeating the self-ownership guard. Resolve via
NameIdentifier (unit tests) then 'sub' (real tokens). Adds a regression test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TDD: wrote 8 tests first (red), then implemented IExpenseService + ExpenseService
covering CRUD, Draft→PendingApproval→Approved→Paid state machine, soft-delete,
per-owner access guards, and receipt blob round-trip via IFileStorage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds IFileStorage abstraction and LocalDiskFileStorage for receipt file storage with path-traversal protection, and registers it in DI. Includes 3 TDD-verified xUnit tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements IMemberService with Create/Read/Update/soft-Delete operations,
NickName/zh-name search, status and hasUser filtering, and full xUnit coverage
(11 tests). Uses separate user-lookup query for InMemory DB compatibility; detaches
entity after soft-delete so query-filter assertions work correctly in tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>