Fix SaaS failing to bill for automation steps containing third party calls

This commit is contained in:
James Brunton
2026-09-02 09:29:01 +01:00
parent 2d94a603ba
commit 6bc4d6ce0f
2 changed files with 34 additions and 1 deletions
@@ -202,7 +202,14 @@ public class PaygChargeInterceptor implements AsyncHandlerInterceptor {
// AI document tools (/api/v1/ai/tools/**) live in the proprietary module and can't
// carry @RequiresFeature, so they're recognised by path — see AiToolRoutes.
boolean aiToolRoute = AiToolRoutes.matches(request);
if (!hasAutoJobPostMapping && !hasRequiresFeature && !aiToolRoute) {
// Any internal automation sub-step (X-Stirling-Automation) is billable automation
// whatever controller it lands on: integration/third-party steps and other proprietary
// tools carry no annotation. Matches self-hosted, which bills by the header regardless.
boolean automationSubStep = hasAutomationHeader(request);
if (!hasAutoJobPostMapping
&& !hasRequiresFeature
&& !aiToolRoute
&& !automationSubStep) {
callsShortCircuit.increment();
return true;
}
@@ -725,6 +725,32 @@ class PaygChargeInterceptorTest {
verify(chargeService, never()).openProcess(any(), anyList());
}
@Test
void preHandle_plainRouteWithAutomationHeader_isAutomation() throws Exception {
// A policy/pipeline sub-step to an unannotated proprietary tool (e.g. an integration step)
// carries X-Stirling-Automation: true. It must be in scope and billed AUTOMATION, matching
// self-hosted, which bills by the header regardless of annotation.
authenticateWithUser(makeUser(7L, 42L));
UUID jobId = UUID.randomUUID();
when(chargeService.openProcess(any(), anyList()))
.thenReturn(new ChargeOutcome(jobId, 1, ChargeOutcome.Disposition.OPENED));
org.mockito.ArgumentCaptor<stirling.software.saas.payg.charge.ChargeContext> ctxCaptor =
org.mockito.ArgumentCaptor.forClass(
stirling.software.saas.payg.charge.ChargeContext.class);
MockMultipartHttpServletRequest req = newMultipart();
req.setRequestURI("/api/v1/integration/external-api-call");
req.addFile(
new MockMultipartFile("fileInput", "x.pdf", "application/pdf", "abc".getBytes()));
req.addHeader("X-Stirling-Automation", "true");
interceptor.preHandle(req, new MockHttpServletResponse(), handlerMethodForPlain());
verify(chargeService).openProcess(ctxCaptor.capture(), anyList());
assertThat(ctxCaptor.getValue().billingCategory())
.isEqualTo(stirling.software.saas.payg.model.BillingCategory.AUTOMATION);
}
// --- helpers --------------------------------------------------------------------------------
private MockMultipartHttpServletRequest newMultipart() {