mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-02 21:03:34 +03:00
saas: wrap long strings + Jackson import reorder
Reflow and wrap long string literals across multiple SaaS modules (logging messages, SQL/JPQL queries, email/content headers, and agreement text) and relocate tools.jackson imports for consistent grouping. These are formatting-only changes to improve line lengths and readability; no functional logic was altered.
This commit is contained in:
@@ -26,9 +26,6 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@@ -53,6 +50,9 @@ import stirling.software.saas.payg.model.JobSource;
|
||||
import stirling.software.saas.payg.model.ProcessType;
|
||||
import stirling.software.saas.util.AuthenticationUtils;
|
||||
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@RestController
|
||||
@Profile("saas")
|
||||
@RequestMapping("/api/v1/ai/create")
|
||||
|
||||
+3
-3
@@ -14,9 +14,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@@ -29,6 +26,9 @@ import stirling.software.saas.ai.service.AiCreateSessionService;
|
||||
import stirling.software.saas.payg.cap.RequiresFeature;
|
||||
import stirling.software.saas.payg.model.FeatureGate;
|
||||
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@RestController
|
||||
@Profile("saas")
|
||||
@RequestMapping("/api/v1/ai/create/internal")
|
||||
|
||||
+6
-4
@@ -12,12 +12,12 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.saas.config.SupabaseConfigurationProperties;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Reports per-tenant overage to Stripe Billing Meters via the Supabase {@code meter-usage} Edge
|
||||
* Function. Only credits consumed above the free tier flow through {@link #reportUsageToStripe}.
|
||||
@@ -58,13 +58,15 @@ public class StripeUsageReportingService {
|
||||
|
||||
if (supabaseUrl == null || supabaseUrl.isEmpty()) {
|
||||
log.error(
|
||||
"[USAGE-BILLING] supabase.url not configured; cannot report usage. Set SUPABASE_URL.");
|
||||
"[USAGE-BILLING] supabase.url not configured; cannot report usage. Set"
|
||||
+ " SUPABASE_URL.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!supabaseConfig.isEdgeFunctionConfigured()) {
|
||||
log.error(
|
||||
"[USAGE-BILLING] Supabase edge function not configured (URL + secret required); cannot report usage.");
|
||||
"[USAGE-BILLING] Supabase edge function not configured (URL + secret required);"
|
||||
+ " cannot report usage.");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,8 @@ public class SaasDataSourceConfig {
|
||||
|
||||
// search_path so native SQL hits stirling_pdf, not the postgres-default public.
|
||||
@Value(
|
||||
"${spring.datasource.hikari.connection-init-sql:SET search_path TO stirling_pdf, auth, public}")
|
||||
"${spring.datasource.hikari.connection-init-sql:SET search_path TO stirling_pdf, auth,"
|
||||
+ " public}")
|
||||
private String connectionInitSql;
|
||||
|
||||
@Bean
|
||||
@@ -59,9 +60,9 @@ public class SaasDataSourceConfig {
|
||||
public DataSource saasDataSource() {
|
||||
if (url == null || url.isBlank()) {
|
||||
throw new IllegalStateException(
|
||||
"spring.datasource.url is required when the saas profile is active. "
|
||||
+ "Set it via application-{profile}.properties (e.g. application-dev.properties) "
|
||||
+ "or via the SPRING_DATASOURCE_URL env var.");
|
||||
"spring.datasource.url is required when the saas profile is active. Set it via"
|
||||
+ " application-{profile}.properties (e.g. application-dev.properties) or"
|
||||
+ " via the SPRING_DATASOURCE_URL env var.");
|
||||
}
|
||||
|
||||
HikariConfig config = new HikariConfig();
|
||||
@@ -79,7 +80,8 @@ public class SaasDataSourceConfig {
|
||||
}
|
||||
|
||||
log.info(
|
||||
"Saas DataSource configured (ApplicationName: '{}', max pool: {}, min idle: {}, search_path init: '{}')",
|
||||
"Saas DataSource configured (ApplicationName: '{}', max pool: {}, min idle: {},"
|
||||
+ " search_path init: '{}')",
|
||||
applicationName,
|
||||
maximumPoolSize,
|
||||
minimumIdle,
|
||||
|
||||
@@ -45,8 +45,9 @@ public class SaasProjectNotice {
|
||||
return;
|
||||
}
|
||||
log.info(
|
||||
"SaaS dev profile: Supabase preview branch {}, ddl-auto={}. Disposable, so Hibernate"
|
||||
+ " is allowed to add the inherited tables the migrations do not create.",
|
||||
"SaaS dev profile: Supabase preview branch {}, ddl-auto={}. Disposable, so"
|
||||
+ " Hibernate is allowed to add the inherited tables the migrations do not"
|
||||
+ " create.",
|
||||
projectRef,
|
||||
ddlAuto);
|
||||
}
|
||||
|
||||
@@ -13,13 +13,13 @@ import java.util.regex.Pattern;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Loads the versioned legal-document registry from {@code legal/manifest.json} on startup and
|
||||
* serves document metadata + rendered markdown from the classpath.
|
||||
@@ -63,8 +63,7 @@ public class LegalDocumentRegistry {
|
||||
d.path("parts"),
|
||||
objectMapper
|
||||
.getTypeFactory()
|
||||
.constructCollectionType(
|
||||
List.class, String.class));
|
||||
.constructCollectionType(List.class, String.class));
|
||||
documents.put(
|
||||
id,
|
||||
new LegalDocumentMeta(
|
||||
|
||||
+2
-2
@@ -20,8 +20,6 @@ import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
|
||||
@@ -40,6 +38,8 @@ import stirling.software.saas.payg.cap.RequiresFeature;
|
||||
import stirling.software.saas.payg.model.FeatureGate;
|
||||
import stirling.software.saas.util.AuthenticationUtils;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Hot-path entitlement check. Runs after {@code PaygChargeInterceptor} in the MVC chain and short-
|
||||
* circuits the request before any handler work happens when the team's snapshot is missing one of
|
||||
|
||||
+4
-2
@@ -155,7 +155,8 @@ public class PaygChargeInterceptor implements AsyncHandlerInterceptor {
|
||||
this.callsBypassed =
|
||||
Counter.builder("payg.filter.bypassed")
|
||||
.description(
|
||||
"Manual UI tool calls that skipped openProcess (BillingCategory.BYPASSED)")
|
||||
"Manual UI tool calls that skipped openProcess"
|
||||
+ " (BillingCategory.BYPASSED)")
|
||||
.register(meterRegistry);
|
||||
this.refundsCounter =
|
||||
Counter.builder("payg.filter.refunds")
|
||||
@@ -429,7 +430,8 @@ public class PaygChargeInterceptor implements AsyncHandlerInterceptor {
|
||||
Long maxBytes = properties.getResponse().getMaxBytes();
|
||||
if (maxBytes != null && wrapper.bytesWritten() > maxBytes) {
|
||||
log.debug(
|
||||
"Response size {} exceeds payg.filter.response.max-bytes={}; skipping OUTPUT recording",
|
||||
"Response size {} exceeds payg.filter.response.max-bytes={}; skipping OUTPUT"
|
||||
+ " recording",
|
||||
wrapper.bytesWritten(),
|
||||
maxBytes);
|
||||
return;
|
||||
|
||||
@@ -181,7 +181,8 @@ public class PaygOutputExtractor {
|
||||
// ZipException extends IOException; IllegalArgumentException covers ZipEntry
|
||||
// "MALFORMED" surfaces from zlib. Fail-open: caller still serves the response.
|
||||
log.debug(
|
||||
"ZIP unpack failed for response body {} ({}); skipping per-PDF OUTPUT recording",
|
||||
"ZIP unpack failed for response body {} ({}); skipping per-PDF OUTPUT"
|
||||
+ " recording",
|
||||
bodyPath,
|
||||
e.getClass().getSimpleName());
|
||||
// Close anything we already opened.
|
||||
|
||||
+4
-2
@@ -91,7 +91,8 @@ public class PaygResponseBodyWrapper extends HttpServletResponseWrapper implemen
|
||||
if (writer != null) {
|
||||
// Servlet spec: getOutputStream() and getWriter() are mutually exclusive per request.
|
||||
throw new IllegalStateException(
|
||||
"getWriter() was already called on this response; cannot switch to getOutputStream()");
|
||||
"getWriter() was already called on this response; cannot switch to"
|
||||
+ " getOutputStream()");
|
||||
}
|
||||
if (teeOut == null) {
|
||||
teeOut = new TeeingServletOutputStream(super.getOutputStream());
|
||||
@@ -103,7 +104,8 @@ public class PaygResponseBodyWrapper extends HttpServletResponseWrapper implemen
|
||||
public PrintWriter getWriter() throws IOException {
|
||||
if (teeOut != null) {
|
||||
throw new IllegalStateException(
|
||||
"getOutputStream() was already called on this response; cannot switch to getWriter()");
|
||||
"getOutputStream() was already called on this response; cannot switch to"
|
||||
+ " getWriter()");
|
||||
}
|
||||
if (writer == null) {
|
||||
String encoding = getCharacterEncoding() != null ? getCharacterEncoding() : "UTF-8";
|
||||
|
||||
+2
-1
@@ -101,7 +101,8 @@ public class InstanceUsageIngestService {
|
||||
// The cumulative counter went backwards — a reset or tampering. Refuse to credit; don't
|
||||
// advance, so the discrepancy stays visible and a corrected resend can reconcile.
|
||||
log.warn(
|
||||
"Instance usage regression team={} category={} reported {} < last {}; ignoring.",
|
||||
"Instance usage regression team={} category={} reported {} < last {};"
|
||||
+ " ignoring.",
|
||||
teamId,
|
||||
category,
|
||||
cumulative,
|
||||
|
||||
+7
-7
@@ -69,7 +69,8 @@ public interface WalletLedgerRepository extends JpaRepository<WalletLedgerEntry,
|
||||
|
||||
/** Sum of signed amounts over a team's entries — the wallet's current balance in units. */
|
||||
@Query(
|
||||
"SELECT COALESCE(SUM(e.amountUnits), 0) FROM WalletLedgerEntry e WHERE e.teamId = :teamId")
|
||||
"SELECT COALESCE(SUM(e.amountUnits), 0) FROM WalletLedgerEntry e WHERE e.teamId ="
|
||||
+ " :teamId")
|
||||
long sumBalanceForTeam(@Param("teamId") Long teamId);
|
||||
|
||||
/** Period-bounded spend for one team in units (debits only). */
|
||||
@@ -92,12 +93,11 @@ public interface WalletLedgerRepository extends JpaRepository<WalletLedgerEntry,
|
||||
* monthly bill + cap.
|
||||
*/
|
||||
@Query(
|
||||
"SELECT COALESCE(SUM(e.amountUnits), 0) FROM WalletLedgerEntry e"
|
||||
+ " WHERE e.teamId = :teamId"
|
||||
+ " AND e.entryType IN (stirling.software.saas.payg.model.LedgerEntryType.DEBIT,"
|
||||
+ " stirling.software.saas.payg.model.LedgerEntryType.REFUND)"
|
||||
+ " AND e.occurredAt >= :periodStart"
|
||||
+ " AND e.occurredAt < :periodEnd")
|
||||
"SELECT COALESCE(SUM(e.amountUnits), 0) FROM WalletLedgerEntry e WHERE e.teamId ="
|
||||
+ " :teamId AND e.entryType IN"
|
||||
+ " (stirling.software.saas.payg.model.LedgerEntryType.DEBIT,"
|
||||
+ " stirling.software.saas.payg.model.LedgerEntryType.REFUND) AND e.occurredAt >="
|
||||
+ " :periodStart AND e.occurredAt < :periodEnd")
|
||||
long sumPeriodNetBillable(
|
||||
@Param("teamId") Long teamId,
|
||||
@Param("periodStart") LocalDateTime periodStart,
|
||||
|
||||
@@ -175,12 +175,10 @@ public class StripeInvoiceDao {
|
||||
}
|
||||
String placeholders = invoiceIds.stream().map(id -> "?").collect(Collectors.joining(","));
|
||||
String sql =
|
||||
"SELECT i.id AS invoice_id,"
|
||||
+ " (SELECT SUM((l->>'quantity')::int)"
|
||||
+ " FROM jsonb_array_elements(COALESCE(i.lines->'data', '[]'::jsonb)) AS l"
|
||||
+ " WHERE l->'price'->'recurring'->>'usage_type' = 'metered') AS qty"
|
||||
+ " FROM stripe.invoices i"
|
||||
+ " WHERE i.id IN ("
|
||||
"SELECT i.id AS invoice_id, (SELECT SUM((l->>'quantity')::int) FROM"
|
||||
+ " jsonb_array_elements(COALESCE(i.lines->'data', '[]'::jsonb)) AS l WHERE"
|
||||
+ " l->'price'->'recurring'->>'usage_type' = 'metered') AS qty FROM"
|
||||
+ " stripe.invoices i WHERE i.id IN ("
|
||||
+ placeholders
|
||||
+ ")";
|
||||
try {
|
||||
|
||||
+4
-4
@@ -18,8 +18,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -43,6 +41,8 @@ import stirling.software.saas.procurement.pricing.QuoteLineItem;
|
||||
import stirling.software.saas.procurement.service.ProcurementService;
|
||||
import stirling.software.saas.util.AuthenticationUtils;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* The enterprise procurement journey for a linked team: read the deal snapshot, start/extend a
|
||||
* (mock-licensed) trial, build a server-priced quote, and accept it. Stripe checkout itself is a
|
||||
@@ -449,7 +449,7 @@ public class ProcurementController {
|
||||
.header(
|
||||
HttpHeaders.CONTENT_DISPOSITION,
|
||||
"attachment;"
|
||||
+ " filename=\"stirling-enterprise-agreement.pdf\"")
|
||||
+ " filename=\"stirling-enterprise-agreement.pdf\"")
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.body(pdf))
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
@@ -472,7 +472,7 @@ public class ProcurementController {
|
||||
.header(
|
||||
HttpHeaders.CONTENT_DISPOSITION,
|
||||
"attachment;"
|
||||
+ " filename=\"stirling-enterprise-agreement.pdf\"")
|
||||
+ " filename=\"stirling-enterprise-agreement.pdf\"")
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.body(pdf))
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
|
||||
+15
-12
@@ -10,8 +10,6 @@ import java.util.Map;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -22,6 +20,8 @@ import stirling.software.saas.procurement.pricing.ProcurementPricingService;
|
||||
import stirling.software.saas.procurement.pricing.QuoteConfig;
|
||||
import stirling.software.saas.procurement.pricing.QuoteLineItem;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Builds the full Stirling Enterprise Agreement for a specific quote: the static MSA (Part A) and
|
||||
* DPA (Part C) from the {@link LegalDocumentRegistry}, with the dynamic Order Form (Part B)
|
||||
@@ -159,7 +159,8 @@ public class AgreementAssembler {
|
||||
row(
|
||||
sb,
|
||||
"Term",
|
||||
"{{term_years}} year(s) · term discount {{term_discount_pct}} on committed processing");
|
||||
"{{term_years}} year(s) · term discount {{term_discount_pct}} on committed"
|
||||
+ " processing");
|
||||
row(sb, "Itemized services", itemizedServices(quote));
|
||||
row(sb, "Annual Fee (year 1)", "{{annual_fee_y1}}");
|
||||
row(sb, "Total (paid in advance)", "{{contract_total}}");
|
||||
@@ -174,12 +175,13 @@ public class AgreementAssembler {
|
||||
sb,
|
||||
"Data schedule",
|
||||
"First 25 MB per file included; each additional 25 MB or part thereof (decimal MB,"
|
||||
+ " rounded up per file, measured once at ingestion) draws down 1 PDF Process."
|
||||
+ " Frozen for the Term (MSA §3.5).");
|
||||
+ " rounded up per file, measured once at ingestion) draws down 1 PDF Process."
|
||||
+ " Frozen for the Term (MSA §3.5).");
|
||||
row(
|
||||
sb,
|
||||
"Drawdown schedule",
|
||||
"{{posture}}: {{processes_per_pdf}} PDF Processes per PDF (MSA §3.3, frozen for the Term)");
|
||||
"{{posture}}: {{processes_per_pdf}} PDF Processes per PDF (MSA §3.3, frozen for the"
|
||||
+ " Term)");
|
||||
row(
|
||||
sb,
|
||||
"Enhanced IP Protection",
|
||||
@@ -187,12 +189,13 @@ public class AgreementAssembler {
|
||||
row(sb, "Standard terms", "SSO, SCIM, RBAC, and audit logs included.");
|
||||
|
||||
sb.append(
|
||||
"\n**Itemized services menu (include as elected):** Self-hosted deployment $12,000/yr"
|
||||
+ " · Air-gapped deployment $36,000/yr · Dedicated SE/CSM $30,000/yr · Enhanced IP"
|
||||
+ " Protection (patent coverage, Section 7.3) 5% of committed processing fees ·"
|
||||
+ " Onboarding & training $7,500 one-time · Quarterly business reviews $8,000/yr."
|
||||
+ " Baseline IP indemnification (copyright, trademark, trade secret) is included at"
|
||||
+ " no charge.\n\n");
|
||||
"\n"
|
||||
+ "**Itemized services menu (include as elected):** Self-hosted deployment"
|
||||
+ " $12,000/yr · Air-gapped deployment $36,000/yr · Dedicated SE/CSM $30,000/yr"
|
||||
+ " · Enhanced IP Protection (patent coverage, Section 7.3) 5% of committed"
|
||||
+ " processing fees · Onboarding & training $7,500 one-time · Quarterly"
|
||||
+ " business reviews $8,000/yr. Baseline IP indemnification (copyright,"
|
||||
+ " trademark, trade secret) is included at no charge.\n\n");
|
||||
sb.append(
|
||||
"**Signatures.** By signing, each signatory represents they have authority to bind"
|
||||
+ " their Party. Signatures delivered electronically or in counterparts are"
|
||||
|
||||
+2
-2
@@ -45,8 +45,8 @@ public class AgreementPdfRenderer {
|
||||
} catch (Exception e) {
|
||||
org.slf4j.LoggerFactory.getLogger(AgreementPdfRenderer.class)
|
||||
.warn(
|
||||
"[legal] agreement PDF render unavailable; recording signature without a"
|
||||
+ " stored PDF: {}",
|
||||
"[legal] agreement PDF render unavailable; recording signature without"
|
||||
+ " a stored PDF: {}",
|
||||
e.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
+6
-5
@@ -16,13 +16,13 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.saas.procurement.config.KeygenConfigurationProperties;
|
||||
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Real {@link EnterpriseLicenseService}: manages the team's enterprise licence directly against the
|
||||
* Keygen API (the "call Keygen from Java" direction), rather than via the Supabase edge functions
|
||||
@@ -53,8 +53,9 @@ public class KeygenEnterpriseLicenseService implements EnterpriseLicenseService
|
||||
// caught at startup, not at the first trial/provision. (Flag off → Mock bean, never here.)
|
||||
if (!config.isConfigured()) {
|
||||
throw new IllegalStateException(
|
||||
"stirling.keygen.enabled=true but Keygen is not fully configured — set "
|
||||
+ "STIRLING_KEYGEN_ACCOUNT_ID / _API_TOKEN / _POLICY_ID, or turn the flag off");
|
||||
"stirling.keygen.enabled=true but Keygen is not fully configured — set"
|
||||
+ " STIRLING_KEYGEN_ACCOUNT_ID / _API_TOKEN / _POLICY_ID, or turn the flag"
|
||||
+ " off");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -55,7 +55,8 @@ public class MockEnterpriseLicenseService implements EnterpriseLicenseService {
|
||||
String ref = existingRef != null ? existingRef : "mock-annual-" + UUID.randomUUID();
|
||||
// Owner email is deliberately not logged — it's PII and adds nothing to the mock trace.
|
||||
log.info(
|
||||
"[procurement][mock-license] issue annual team={} seats={} volume={} deployment={} expires={} ref={} upgrade={}",
|
||||
"[procurement][mock-license] issue annual team={} seats={} volume={} deployment={}"
|
||||
+ " expires={} ref={} upgrade={}",
|
||||
teamId,
|
||||
ent.seats(),
|
||||
ent.volume(),
|
||||
|
||||
+3
-3
@@ -10,9 +10,6 @@ import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.enumeration.TeamRole;
|
||||
@@ -38,6 +35,9 @@ import stirling.software.saas.procurement.repository.ProcurementQuoteRepository;
|
||||
import stirling.software.saas.service.SaasTeamService;
|
||||
import stirling.software.saas.util.LogRedactionUtils;
|
||||
|
||||
import tools.jackson.core.JacksonException;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Orchestrates a linked team's procurement journey: start a (mock-licensed) trial, build a
|
||||
* server-priced quote, and accept it. Stripe checkout itself lives in a Supabase edge function the
|
||||
|
||||
+2
-1
@@ -21,7 +21,8 @@ public interface SupabaseUserRepository extends JpaRepository<SupabaseUser, UUID
|
||||
* anonymous sessions in batch (avoids long-running transactions on a single big delete).
|
||||
*/
|
||||
@Query(
|
||||
"SELECT s.id FROM SupabaseUser s WHERE s.isAnonymous = true AND s.createdAt < :cutoffDate")
|
||||
"SELECT s.id FROM SupabaseUser s WHERE s.isAnonymous = true AND s.createdAt <"
|
||||
+ " :cutoffDate")
|
||||
Stream<UUID> findByCreatedAtBeforeAndIsAnonymousTrue(
|
||||
@Param("cutoffDate") LocalDateTime cutoffDate);
|
||||
|
||||
|
||||
+10
-6
@@ -23,7 +23,8 @@ public interface TeamInvitationRepository extends JpaRepository<TeamInvitation,
|
||||
* @return Optional of TeamInvitation if found
|
||||
*/
|
||||
@Query(
|
||||
"SELECT ti FROM TeamInvitation ti JOIN FETCH ti.team JOIN FETCH ti.inviter WHERE ti.invitationToken = :token")
|
||||
"SELECT ti FROM TeamInvitation ti JOIN FETCH ti.team JOIN FETCH ti.inviter WHERE"
|
||||
+ " ti.invitationToken = :token")
|
||||
Optional<TeamInvitation> findByInvitationToken(@Param("token") String token);
|
||||
|
||||
/**
|
||||
@@ -33,7 +34,8 @@ public interface TeamInvitationRepository extends JpaRepository<TeamInvitation,
|
||||
* @return List of invitations
|
||||
*/
|
||||
@Query(
|
||||
"SELECT ti FROM TeamInvitation ti JOIN FETCH ti.team JOIN FETCH ti.inviter WHERE ti.inviteeEmail = :email")
|
||||
"SELECT ti FROM TeamInvitation ti JOIN FETCH ti.team JOIN FETCH ti.inviter WHERE"
|
||||
+ " ti.inviteeEmail = :email")
|
||||
List<TeamInvitation> findByInviteeEmail(@Param("email") String email);
|
||||
|
||||
/**
|
||||
@@ -43,8 +45,8 @@ public interface TeamInvitationRepository extends JpaRepository<TeamInvitation,
|
||||
* @return List of pending invitations
|
||||
*/
|
||||
@Query(
|
||||
"SELECT ti FROM TeamInvitation ti JOIN FETCH ti.team JOIN FETCH ti.inviter "
|
||||
+ "WHERE ti.inviteeEmail = :email AND ti.status = 'PENDING' AND ti.expiresAt > :now")
|
||||
"SELECT ti FROM TeamInvitation ti JOIN FETCH ti.team JOIN FETCH ti.inviter WHERE"
|
||||
+ " ti.inviteeEmail = :email AND ti.status = 'PENDING' AND ti.expiresAt > :now")
|
||||
List<TeamInvitation> findPendingInvitationsByEmail(
|
||||
@Param("email") String email, @Param("now") LocalDateTime now);
|
||||
|
||||
@@ -64,7 +66,8 @@ public interface TeamInvitationRepository extends JpaRepository<TeamInvitation,
|
||||
* @return List of invitations
|
||||
*/
|
||||
@Query(
|
||||
"SELECT ti FROM TeamInvitation ti JOIN FETCH ti.team WHERE ti.inviter.id = :inviterUserId")
|
||||
"SELECT ti FROM TeamInvitation ti JOIN FETCH ti.team WHERE ti.inviter.id ="
|
||||
+ " :inviterUserId")
|
||||
List<TeamInvitation> findByInviterUserId(@Param("inviterUserId") Long inviterUserId);
|
||||
|
||||
/**
|
||||
@@ -75,7 +78,8 @@ public interface TeamInvitationRepository extends JpaRepository<TeamInvitation,
|
||||
*/
|
||||
@Modifying
|
||||
@Query(
|
||||
"UPDATE TeamInvitation ti SET ti.status = 'EXPIRED' WHERE ti.status = 'PENDING' AND ti.expiresAt < :now")
|
||||
"UPDATE TeamInvitation ti SET ti.status = 'EXPIRED' WHERE ti.status = 'PENDING' AND"
|
||||
+ " ti.expiresAt < :now")
|
||||
int markExpiredInvitations(@Param("now") LocalDateTime now);
|
||||
|
||||
/**
|
||||
|
||||
+2
-1
@@ -383,7 +383,8 @@ public class SupabaseAuthenticationFilter extends OncePerRequestFilter {
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new AuthenticationFailureException(
|
||||
"User creation conflict, but unable to find existing user",
|
||||
"User creation conflict, but unable to find existing"
|
||||
+ " user",
|
||||
dup));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,8 +174,9 @@ public class SupabaseSecurityConfig {
|
||||
String issuerError = validateIssuer(issuer);
|
||||
if (issuerError != null) {
|
||||
log.warn(
|
||||
"{} saas profile is active but JWTs cannot be validated. Set SAAS_DB_PROJECT_REF"
|
||||
+ " (or app.supabase.issuer) in application-saas.properties or via env.",
|
||||
"{} saas profile is active but JWTs cannot be validated. Set"
|
||||
+ " SAAS_DB_PROJECT_REF (or app.supabase.issuer) in"
|
||||
+ " application-saas.properties or via env.",
|
||||
issuerError);
|
||||
// Build a decoder that will reject every token; failing closed is safer than failing
|
||||
// open when configuration is incomplete.
|
||||
|
||||
@@ -274,7 +274,8 @@ public class SaasTeamService {
|
||||
existingUser -> {
|
||||
if (hasPaidSubscription(existingUser)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot invite paid users to teams. Only team leaders manage billing.");
|
||||
"Cannot invite paid users to teams. Only team leaders"
|
||||
+ " manage billing.");
|
||||
}
|
||||
|
||||
// Check if already a member
|
||||
@@ -324,7 +325,8 @@ public class SaasTeamService {
|
||||
Team userTeam = user.getTeam();
|
||||
if (userTeam == null || !hasActivePaidSubscription(userTeam)) {
|
||||
log.warn(
|
||||
"User {} joined team {} but team has no active subscription - not granting PRO role",
|
||||
"User {} joined team {} but team has no active subscription - not granting PRO"
|
||||
+ " role",
|
||||
user.getUsername(),
|
||||
userTeam != null ? userTeam.getName() : "null");
|
||||
return;
|
||||
@@ -386,7 +388,8 @@ public class SaasTeamService {
|
||||
// Validate: user doesn't have paid subscription
|
||||
if (hasPaidSubscription(acceptingUser)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot join team with active paid subscription. Cancel your subscription first.");
|
||||
"Cannot join team with active paid subscription. Cancel your subscription"
|
||||
+ " first.");
|
||||
}
|
||||
|
||||
// Validate: team has available seats
|
||||
@@ -683,9 +686,9 @@ public class SaasTeamService {
|
||||
// Check if Supabase is configured
|
||||
if (!supabaseConfig.isEdgeFunctionConfigured()) {
|
||||
log.warn(
|
||||
"Supabase integration not configured, skipping email send. "
|
||||
+ "Please configure supabase.edgeFunctionUrl and supabase.edgeFunctionSecret "
|
||||
+ "in application properties.");
|
||||
"Supabase integration not configured, skipping email send. Please configure"
|
||||
+ " supabase.edgeFunctionUrl and supabase.edgeFunctionSecret in"
|
||||
+ " application properties.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -748,7 +751,8 @@ public class SaasTeamService {
|
||||
if (maxSeats < currentSeatsUsed) {
|
||||
int excessMembers = currentSeatsUsed - maxSeats;
|
||||
log.warn(
|
||||
"Team {} reducing seats from {} to {} with {} current members. Removing {} excess members.",
|
||||
"Team {} reducing seats from {} to {} with {} current members. Removing {}"
|
||||
+ " excess members.",
|
||||
teamId,
|
||||
currentMaxSeats,
|
||||
maxSeats,
|
||||
@@ -811,7 +815,8 @@ public class SaasTeamService {
|
||||
downgradeUserToFree(userToRemove);
|
||||
|
||||
log.info(
|
||||
"User {} removed from team {} and migrated to personal team due to seat reduction",
|
||||
"User {} removed from team {} and migrated to personal team due to seat"
|
||||
+ " reduction",
|
||||
userToRemove.getId(),
|
||||
teamId);
|
||||
}
|
||||
@@ -886,7 +891,8 @@ public class SaasTeamService {
|
||||
|
||||
if (hasActiveSubscription) {
|
||||
log.info(
|
||||
"User {} has active subscription (trial or paid), maintaining PRO access after leaving team",
|
||||
"User {} has active subscription (trial or paid), maintaining PRO"
|
||||
+ " access after leaving team",
|
||||
refetchedUser.getUsername());
|
||||
return; // Keep PRO access
|
||||
}
|
||||
|
||||
@@ -121,7 +121,8 @@ class SaasSchemaOwnershipTest {
|
||||
will not exist on a fresh preview branch.
|
||||
- HIBERNATE_MANAGED: only correct for a table inherited from the \
|
||||
self-hosted app that no Supabase migration creates.
|
||||
Offending tables -> entities: %s"""
|
||||
Offending tables -> entities: %s\
|
||||
"""
|
||||
.formatted(
|
||||
undeclared.stream()
|
||||
.map(t -> t + " (" + mapped.get(t) + ")")
|
||||
|
||||
+3
-3
@@ -28,9 +28,6 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
|
||||
@@ -45,6 +42,9 @@ import stirling.software.saas.payg.model.FeatureGate;
|
||||
import stirling.software.saas.payg.model.FeatureSet;
|
||||
import stirling.software.saas.security.EnhancedJwtAuthenticationToken;
|
||||
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Pure-Mockito tests for {@link EntitlementGuard}. Covers the four decision-matrix cells: anonymous
|
||||
* billable → 401, anonymous manual → pass, authenticated FULL → pass, authenticated DEGRADED for a
|
||||
|
||||
@@ -1433,8 +1433,8 @@ class SaasTeamServiceTest {
|
||||
assertThatThrownBy(() -> service.acceptInvitation(TOKEN, joiner))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage(
|
||||
"Revoke linked self-hosted instances on this team before joining another"
|
||||
+ " team.");
|
||||
"Revoke linked self-hosted instances on this team before joining"
|
||||
+ " another team.");
|
||||
|
||||
// Guard fires before any team mutation.
|
||||
verify(membershipRepository, never()).delete(any());
|
||||
|
||||
+4
-2
@@ -178,7 +178,8 @@ class SaasUserAccountServiceTest {
|
||||
|
||||
@Test
|
||||
@DisplayName(
|
||||
"downgrades a PRO user whose team is personal (personal team is not a shared PRO source)")
|
||||
"downgrades a PRO user whose team is personal (personal team is not a shared PRO"
|
||||
+ " source)")
|
||||
void proWithPersonalTeam_isDowngraded() {
|
||||
User u = userWithRole(Role.PRO_USER.getRoleId());
|
||||
Team t = team(7L, "alice-personal");
|
||||
@@ -414,7 +415,8 @@ class SaasUserAccountServiceTest {
|
||||
|
||||
@Test
|
||||
@DisplayName(
|
||||
"does not overwrite username/email when the email is blank, but still promotes the type")
|
||||
"does not overwrite username/email when the email is blank, but still promotes the"
|
||||
+ " type")
|
||||
void blankEmail_keepsUsername() {
|
||||
SupabaseUser su = supabaseUser(false);
|
||||
User u = anonymousLocalUser();
|
||||
|
||||
Reference in New Issue
Block a user