Checking access…

Skip to main content
Version: v2

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 501 for 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.

Do not bypass this control

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​

EnvironmentV2 Endpoints AvailableBehaviour
devβœ… YesImplemented endpoints accessible; stubs return 501
testβœ… YesImplemented endpoints accessible; stubs return 501
regβœ… YesImplemented endpoints accessible; stubs return 501
perfβœ… YesImplemented endpoints accessible; stubs return 501
stageConfirm per operationValidate stage routing and readiness independently
prodβœ… Operation-specificProduction-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:

DetailValue
Annotation resolutionLooks up the matched handler method via RequestMappingHandlerMapping
No handler foundPasses 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.

Why explicit opt-in instead of auto-configuration?

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 @EnableV2EndpointControls finds all participating services immediately.
  • No future service accidentally picks up the filters as a transitive classpath side-effect.
Non-reactive services

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:

RuleExample
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 charactersKeep 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:

  1. Delete @V2NotImplemented(reason = "...") from the method.
  2. Replace return Mono.empty(); with the real implementation.
  3. Update the V2 API Environment Availability endpoint table from πŸ”§ Stub (501) to βœ… Implemented.
  4. 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 classScopeKey assertions
V2ProductionAvailabilityIT@ActiveProfiles("prod")Each approved V2 operation reaches its handler; no blanket 503 block
V2NotImplementedFilterITEach routed environmentStub endpoints return 501 where routed; implemented endpoints are not intercepted
V2MerchantEnablementITProduction-like profileDisabled 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.