V2 Endpoint Environment Controls
π InternalInternal information β not visible in the public (merchant) site.
Purposeβ
V2 is available in production. Readiness is controlled per operation and merchant, not by a blanket environment block. Every V2 service must:
- Route only operations approved for the target environment.
- Confirm merchant enablement before production cutover.
- Return
501for routed handlers annotated as not implemented. - Keep deferred operations out of migration plans even when their contracts are published.
Stage status is validated separately and must not be inferred from production availability.
Never remove the @V2NotImplemented annotation from a stub without a completed, reviewed, and tested implementation. Never treat a published route or the general production availability of V2 as approval for a deferred operation.
Environment Access Matrixβ
| Environment | V2 Endpoints Available | Behaviour |
|---|---|---|
dev | β Yes | Implemented endpoints accessible; stubs return 501 |
test | β Yes | Implemented endpoints accessible; stubs return 501 |
reg | β Yes | Implemented endpoints accessible; stubs return 501 |
perf | β Yes | Implemented endpoints accessible; stubs return 501 |
stage | Confirm per operation | Validate stage routing and readiness independently |
prod | β Operation-specific | Production-ready operations are accessible when enabled for the merchant; deferred operations must not be used |
For the consumer-facing description of this matrix (what API callers receive), see V2 API Environment Availability.
Architecture β Routing and Operation Controlsβ
The runtime model separates route exposure from operation readiness:
Incoming /v2/ request
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Layer 1: Istio VirtualService (gateway / infrastructure)β
β Match the public path and route it to the owning service β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Layer 2: Merchant and operation readiness β
β Verify target-environment approval and enablement β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β (enabled operations)
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Layer 3: V2NotImplementedFilter @Order(-100) (app) β
β Handler annotated @V2NotImplemented β 501 JSON response β
β Handler not annotated β pass through β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β (implemented endpoints only)
βΌ
Controller handler executes normally
Route configuration and handler readiness are independent. A published route can still resolve to a deferred handler, and an implemented operation can still require merchant enablement.
Implementation β Stub Filterβ
V2NotImplementedFilter is provided by wallet-event-commons. Services annotate stub methods instead of implementing local interception logic.
V2NotImplementedFilter β Per-Endpoint Stub Blockβ
// com.optum.wallet.common.v2.webfilter.V2NotImplementedFilter
@Order(-100)
public class V2NotImplementedFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// Resolves the handler for this request, then checks for @V2NotImplemented
// Uses defaultIfEmpty(sentinel) to avoid the Mono<Void> always-empty trap
return handlerMappingProvider.getObject()
.getHandler(exchange)
.defaultIfEmpty(NO_HANDLER_SENTINEL)
.flatMap(handler -> {
if (handler == NO_HANDLER_SENTINEL) return chain.filter(exchange);
V2NotImplemented annotation = resolveAnnotation(handler);
if (annotation != null) {
return writeErrorResponse(exchange, 501, "V2_ENDPOINT_NOT_IMPLEMENTED", annotation.reason());
}
return chain.filter(exchange);
});
}
}
Key behaviour:
| Detail | Value |
|---|---|
| Annotation resolution | Looks up the matched handler method via RequestMappingHandlerMapping |
| No handler found | Passes through (returns to chain; 404 is handled elsewhere) |
| Filter order | @Order(-100) β runs after env check, before Spring Security |
Filter Orderingβ
Spring WebFilter beans execute in ascending @Order value β lower number = earlier execution.
@Order(-100) V2NotImplementedFilter β runs second
@Order(-100) Spring Security β default Spring Security order
@Order(0+) Tracing, CORS, etc.
The negative order runs the stub check before Spring framework defaults.
Opt-In Registration β @EnableV2EndpointControlsβ
Both filters are provided by wallet-event-commons. Services register them by adding the @EnableV2EndpointControls annotation to their main application class β the same pattern used by @EnableScheduling, @EnableWebFlux, and other Spring meta-annotations.
// Every CCG reactive service that exposes V2 endpoints
@SpringBootApplication
@EnableWebFlux
@EnableScheduling
@EnableV2EndpointControls // β explicit opt-in
public class PaymentApplication { ... }
This imports V2EndpointConfiguration, which registers the operation-control filter:
// com.optum.wallet.common.v2.webfilter.V2EndpointConfiguration
@Configuration
@ConditionalOnWebApplication(type = REACTIVE)
public class V2EndpointConfiguration {
@Bean @Order(-100)
public V2NotImplementedFilter v2NotImplementedFilter(
ObjectMapper objectMapper,
@Qualifier("requestMappingHandlerMapping") ObjectProvider<RequestMappingHandlerMapping> handlerMappingProvider) {
return new V2NotImplementedFilter(objectMapper, handlerMappingProvider);
}
}
Services do not write any filter code β they add the annotation to their main class and annotate stub methods. Deployments must not register a blanket production V2EndpointFilter rule.
The original implementation used AutoConfiguration.imports (classpath-based silent registration). This was replaced with an explicit annotation so that:
- Every service consciously opts in β a missing annotation is a visible omission, not a silent gap.
- The registration is greppable:
grep -r @EnableV2EndpointControlsfinds all participating services immediately. - No future service accidentally picks up the filters as a transitive classpath side-effect.
The @ConditionalOnWebApplication(REACTIVE) guard means V2EndpointConfiguration is a no-op if imported in a non-WebFlux service. The annotation is still harmless to add, but it has no effect.
How to Declare a Stub Endpointβ
When implementing a V2 controller method that is routed but not yet implemented, follow this pattern:
Step 1 β Annotate the handler methodβ
@Operation(summary = "Capture Split-Tender Payment")
@PostMapping("/{paymentId}/capture")
@V2NotImplemented(reason = "Capture for split-tender V2 is under active development and not yet available for consumption.")
public Mono<ResponseEntity<?>> capturePayment(
@RequestHeader("X-Merchant-Id") UUID merchantId,
@PathVariable UUID paymentId) {
return Mono.just(ResponseEntity.status(HttpStatus.NOT_IMPLEMENTED).build()); // unreachable β filter intercepts first
}
Step 2 β Write a meaningful reasonβ
The reason value appears verbatim in the detail field of the 501 response body seen by API consumers. It must:
| Rule | Example |
|---|---|
| Explain what is not yet available | "Capture for split-tender V2 is under active development." |
| Be specific to the endpoint | β Generic "Not implemented" β β |
| Be < 200 characters | Keep it concise |
Step 3 β The method body must satisfy the return typeβ
The method body is unreachable (the filter returns before Spring dispatches to the handler), but it must compile and satisfy the return type contract. Return a NOT_IMPLEMENTED response:
return Mono.just(ResponseEntity.status(HttpStatus.NOT_IMPLEMENTED).build()); // unreachable β V2NotImplementedFilter intercepts
Step 4 β Update the endpoint status tableβ
Add or update the endpoint in the V2 API Environment Availability page, marking it as π§ Stub (501).
How to Promote a Stub to Implementedβ
When an endpoint is ready for active consumption, remove the annotation and implement the handler:
- Delete
@V2NotImplemented(reason = "...")from the method. - Replace
return Mono.empty();with the real implementation. - Update the V2 API Environment Availability endpoint table from
π§ Stub (501)toβ Implemented. - Ensure the PR includes the test coverage described in Testing Requirements below.
Testing Requirementsβ
Every service must have integration tests covering production routing for approved operations and stub behaviour. These tests must not be skipped, and they must use real Spring context startup.
Required test classesβ
| Test class | Scope | Key assertions |
|---|---|---|
V2ProductionAvailabilityIT | @ActiveProfiles("prod") | Each approved V2 operation reaches its handler; no blanket 503 block |
V2NotImplementedFilterIT | Each routed environment | Stub endpoints return 501 where routed; implemented endpoints are not intercepted |
V2MerchantEnablementIT | Production-like profile | Disabled and enabled merchant behavior matches the operation contract |
Minimum assertions per testβ
// Production-ready operation β verify it is not blanket-blocked
@Test
void createPayment_inProduction_isRouted() {
webTestClient.post().uri("/v2/payments")
.bodyValue(validRequest)
.exchange()
.expectStatus().isNotEqualTo(503);
}
// Stub endpoint β verify 501 where the operation is routed
@Test
void deferredCheckoutSessionOperation_returns501() {
webTestClient.patch().uri("/v2/checkout-sessions/{id}/cancel", sessionId)
.exchange()
.expectStatus().isEqualTo(501)
.expectBody()
.jsonPath("$.title").isEqualTo("V2_ENDPOINT_NOT_IMPLEMENTED");
}
Infrastructure Routingβ
The ccg-api-v2 VirtualService routes approved public paths to their owning services. Production values must not apply a blanket HTTPFaultInjection.Abort to all V2 routes. Route reviews must verify the exact method, path rewrite, destination service, target environment, and merchant enablement dependency for each operation.
Stage and production values are reviewed independently. Enabling production routing does not establish stage readiness, and route presence does not override @V2NotImplemented.
Relatedβ
- V2 API Environment Availability β consumer-facing version of this information
- Commons Library β
wallet-event-commonsβv2/webfilterpackage details - CC-22996 β V2 Endpoint Environment Controls