Skip to content

Slice 005: pay with Tendr - #19

Merged
erikshafer merged 2 commits into
mainfrom
slice-005-pay-with-tendr
Sep 14, 2026
Merged

erikshafer merged 2 commits into
mainfrom
slice-005-pay-with-tendr

Conversation

@erikshafer

Copy link
Copy Markdown
Owner

Slice 005, the first v1.1 change after KCDC: both twins call a third-party card vendor over HTTP on PlaceOrder's load leg, and the shared suite runs every payment scenario against a real vendor host on a real socket. Spec: docs/slices/005-pay-with-tendr.md. Decisions and evidence: the two 2026-09-14 entries in docs/build-log.md.

What changed

  • Tendr (src/tendr/Tendr, port 42040): a fake card vendor on Wolverine.Http and Marten's event store, schema tendr. Idempotent POST /v1/authorizations, GET /v1/authorizations/{id}, a four-entry test-card table. Not a twin, not in the scorecard.
  • Contract: POST /orders takes an optional card. No card behaves exactly as before, and the body has no payment member. A declined card is a 402 after the three existing guards; a vendor that refuses the connection or runs past the two-second timeout is a 503. GET /orders/{id} mirrors the payment.
  • Before twin (11 created, 8 edited): IPaymentGateway port and PaymentAuthorization outcome in Application, TendrPaymentGateway typed client with options and wire DTOs in Infrastructure, two exceptions mapped in the filter, PaymentStatus enum, PaymentDto, migration AddOrderPayment. The handler now injects eight dependencies.
  • After twin (1 created, 3 edited): Payments/Tendr.cs; in PlaceOrder.cs a [WolverineBefore] AuthorizeAsync rung and a Validate(Payment?) guard; Decide, LoadAsync and the three existing guards are unchanged. Generated code confirms the chain runs LoadAsync, Validate, AuthorizeAsync, Validate, Post.
  • Suite: new PaymentScenarios (6) and PaymentOutageScenarios (2), both twins. OrderScenarios.cs and every other existing scenario file have no diff, so the Marten and template experiments are untouched.
  • Optional step, own commit: Tendr also answers AuthorizeCard over Wolverine's HTTP transport, and tests/Tendr.Tests proves InvokeAsync<CardAuthorization> end to end on the pinned 6.30.0.
  • No package added or bumped. OpenSpec change archived and the orders spec synced. README, CLAUDE.md, inventory, frontend page and spec updated.

Results

  • dotnet test: contract 70/70 (35 x 2), after-twin facts 19/19, Tendr 20/20, 0 warnings. About 16 s warm and 28 s cold, against 11 to 12 s and 20 s for main measured on the same machine.
  • Experiments: Marten 27/27 and 15/15, template 6/10 with the same four reds as committed.
  • Whole twins: before 2,405 raw / 2,002 non-blank (was 2,140 / 1,775), after 1,000 / 841 (was 844 / 707). The ratio moves from about 2.5x to about 2.4x.
  • Live against compose: approved, declined, vendor-down and timeout paths on both twins, plus the demo page's new Pay now select.

Worth a look

  • Post keeps an unused PlaceOrder command parameter, because Wolverine.Http reads the body type from the endpoint method's own parameters.
  • The after twin needs AlwaysUseServiceLocationFor<TendrClient>(), because a typed HttpClient is an opaque factory to Wolverine 6's codegen.
  • On the after twin the EF Core transaction is open while Tendr is called (up to two seconds). The before twin opens its transaction only at save.
  • The after twin's first live start dropped after.orders in the compose database (one test order lost) because the new NOT NULL column had no default. Fixed with a column default, verified against a throwaway database.
  • tests/Tendr.Tests in the slnx adds one PostgreSQL container, at no measurable wall-clock cost. Removing it from the slnx is one line if the live run should stay at four containers.

…th twins

Tendr (src/tendr/Tendr, port 42040) is a fake card vendor on Wolverine.Http
and Marten's event store: idempotent POST /v1/authorizations keyed by the
order id, a four-entry test-card table, GET by id. Not a twin, not counted.

POST /orders takes an optional card. No card behaves exactly as before.
A declined card is a 402 after the three existing guards; a vendor that
refuses the connection or runs past the two-second timeout is a 503.

Before twin: IPaymentGateway port and PaymentAuthorization in Application,
TendrPaymentGateway typed client with options and wire DTOs in
Infrastructure, two exceptions mapped in the filter, migration
AddOrderPayment. 11 files created, 8 edited.

After twin: Payments/Tendr.cs, plus an AuthorizeAsync rung and a
Validate(Payment?) guard in PlaceOrder.cs. Decide and the three guards are
unchanged. 1 file created, 3 edited. The column default on PaymentStatus
keeps Wolverine's startup migration from dropping a populated orders table.

Suite: 70/70 contract (35 x 2, the 27 existing scenarios unchanged), 19/19
after-twin facts, 19/19 Tendr. A Tendr host per twin collection runs on
Kestrel against that collection's PostgreSQL; Wolverine hosts in the test
process are built one at a time so each keeps its own application assembly.

OpenSpec change archived; orders spec synced. Inventory, build log, README,
CLAUDE.md, frontend spec and demo page updated.
…keAsync

The optional step from the slice 005 hand-off, kept to Tendr; neither twin
changes. Tendr maps MapWolverineHttpTransportEndpoints() beside its REST
endpoints, and a thin AuthorizeCardHandler answers AuthorizeCard sent as a
message, using the envelope id as the idempotency key. The request rules and
the stream start moved into AuthorizeCardEndpoint.Invalid and Start so the
REST call and the message share them.

tests/Tendr.Tests/HttpTransportFacts.cs: a plain Wolverine sender host routes
AuthorizeCard to Tendr's /_wolverine/invoke and gets an approval back from
InvokeAsync<CardAuthorization>; Tendr's store holds the authorization after.

On the pinned WolverineFx.Http 6.30.0 the HTTP transport resolves only
https:// endpoints (http:// is first handled in 6.34.0), so the test serves
Tendr over HTTPS with a certificate it generates itself. No package bump.

Root dotnet test: 70/70, 19/19, Tendr.Tests 20/20. Also corrects the slice
spec: OrderConfiguration is unchanged, and the after twin's PaymentStatus
column carries a database default.
@erikshafer erikshafer self-assigned this Sep 14, 2026
@erikshafer
erikshafer merged commit 46f453e into main Sep 14, 2026
1 check passed
@erikshafer
erikshafer deleted the slice-005-pay-with-tendr branch September 14, 2026 20:49
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.

1 participant