spring.application.name=udyamlinc-backend server.port=8082 server.servlet.context-path=/api/v1/udyam # JPA & Hibernate spring.jpa.hibernate.ddl-auto=update spring.jpa.properties.hibernate.default_schema=public spring.jpa.open-in-view=false # DataSource — UdyamLinc_DEMO spring.datasource.url=${UDYAM_DB_URL:jdbc:postgresql://localhost:5432/UdyamLinc_DEMO} spring.datasource.username=${UDYAM_DB_USERNAME:postgres} spring.datasource.password=${UDYAM_DB_PASSWORD:root} spring.datasource.driver-class-name=org.postgresql.Driver spring.datasource.type=com.zaxxer.hikari.HikariDataSource # HikariCP spring.datasource.hikari.connection-timeout=10000 spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.maximum-pool-size=20 spring.datasource.hikari.idle-timeout=600000 spring.datasource.hikari.max-lifetime=1800000 spring.datasource.hikari.auto-commit=true # throw NoHandlerFoundException for 404s spring.mvc.throw-exception-if-no-handler-found=true # Demo signing key, overridable by environment. The default is committed on # purpose so a fresh clone boots without setup — which also means it is public, # and therefore not a secret. Any deployment carrying real borrower data must set # UDYAM_JWT_SECRET; a key readable from the repository signs anyone's token. jwt.secret=${UDYAM_JWT_SECRET:55647961_6D4C_696E_6320_4E41_4241_5244_5F32_3032_365F_4C69_6E63_4675_7369_6F6E_5F48_6163_6B61_74686F6E} # 24 hours jwt.expiration=86400000 # The Vite dev server for UdyamLinc-UI (5180 is BharatLinc) udyam.cors.allowed-origins=http://localhost:5182,http://127.0.0.1:5182,http://localhost:4173 # Forecast horizon in months — the deck's 3–6 month early-warning window udyam.forecast.horizon-months=6 # Composite Stress Score band thresholds; map these to the institution's watchlist udyam.risk.red-threshold=65 udyam.risk.watch-threshold=35 # ── Aggregate suppression ─────────────────────────────────── # Fewest borrowers in a cell before its numbers may be published. Below this the # cell reports "suppressed" rather than a rounded figure, because a district of # two borrowers and one alert names that borrower as surely as printing them. udyam.apex.min-cell-size=5 # The same threshold for a Regional Office session, separated so it can be tuned # without touching the national one. # # It defaults to the SAME value on purpose. Suppression protects against # re-identifying a borrower from an aggregate, and that risk turns on what the # reader already knows — a regional officer, supervising four districts and on # first-name terms with the banks in them, can re-identify from a cell of three # far more easily than someone reading a national table. So the argument from # proximity runs the other way: if these two ever diverge, the regional figure # has a better case for being HIGHER than the national one, not lower. # # Lower it only if a supervisor has decided that trade-off deliberately, and # knows that a small office's sector cells are where it will show. udyam.apex.min-cell-size.regional-office=5 # ── Risk & rule engine (ARCHITECTURE Phase D) ─────────────── # Which ScoringStrategy binds. RULE today; MODEL when the LightGBM heads land — # that swap is this line, not a rewrite. udyam.risk.engine=RULE udyam.risk.model-version=rule-v1.0.0 # Where a family lands with no usable feature. Neutral, not innocent: scoring 0 # would read as "safe on this dimension" when the truth is that we do not know. udyam.risk.family-prior=40 # A rule engine reports low confidence by design. udyam.risk.rule-confidence-ceiling=70 # Nightly scoring, an hour behind ingestion so it scores what that job wrote. udyam.risk.scheduled=${UDYAM_SCORING_ENABLED:false} udyam.risk.cron=0 0 3 * * * # Seed the demo portfolio on first boot: six states, one per NABARD zone, each # carrying 18 enterprises across the five sector packs (108 in all). udyam.seed.enabled=true # Destructive. When true, the seeder deletes any enterprise not in PortfolioPlan # along with its runs, alerts, visits, ledger and loan rows. Needed for exactly # one boot when the portfolio's shape changes — the move from 142 Madhya Pradesh # enterprises to 108 across six states is such a change. Turn it off again after. udyam.seed.reshape=${UDYAM_SEED_RESHAPE:false} # ── Ingestion (ARCHITECTURE Phase B) ──────────────────────── # Off by default: a demo boot must never call a government endpoint. Turn on for # the pilot, or trigger a one-off pass with POST /admin/ingestion/run. udyam.ingestion.enabled=${UDYAM_INGESTION_ENABLED:false} udyam.ingestion.cron=0 0 2 * * * udyam.ingestion.zone=Asia/Kolkata udyam.ingestion.http.connect-timeout-ms=10000 udyam.ingestion.http.read-timeout-ms=30000 # Agmarknet mandi prices — data.gov.in Open Government Data API. # Register at https://data.gov.in for a key; without one the run records SKIPPED. udyam.ingestion.datagov.api-key=${DATAGOV_API_KEY:} udyam.ingestion.datagov.base-url=https://api.data.gov.in/resource # "Variety-wise Daily Market Prices Data of Commodity" — the historical Agmarknet # resource, ~81M rows, current to yesterday. NOT 9ef84268-…, which is "Current # Daily Price": a today-only snapshot carrying a single arrival_date, from which # no week-over-week history can be built and in which none of the four pilot # districts appear. udyam.ingestion.datagov.resource-id=${DATAGOV_RESOURCE_ID:35985678-0d79-46b4-9ed6-6f13308a1d24} udyam.ingestion.datagov.page-limit=1000 # Page ceiling per (district, commodity). Only reached when a run asks for a deep # lookback — POST /admin/ingestion/run/AGMARKNET?lookbackWeeks=156 is the one-off # three-year backfill that gives price_z_score its seasonal mean. 40 pages of # 1000 rows is ample for three years of one district's arrivals. udyam.ingestion.datagov.max-pages=40 # Filter names must match the resource's own field names; they differ by resource # version. This resource capitalises them, and — unlike the current-price # resource — matches exactly on the bare name. Appending ".keyword" here returns # zero rows; omitting the capital returns rows from every state whose name shares # a token, so "Madhya Pradesh" would silently pull Andhra, Himachal and Uttar. udyam.ingestion.datagov.field.state=State udyam.ingestion.datagov.field.district=District udyam.ingestion.datagov.field.commodity=Commodity # Rows come back in arbitrary order and the lookback filter is applied client # side, so without a descending sort a 1000-row page is mostly years-old data and # every commodity reports zero weeks written. udyam.ingestion.datagov.sort-field=Arrival_Date # data.gov.in meters by quota per window, so the limit is reached by the run and # not by any one call — once it trips, every remaining pair in that run fails # together and the whole ingestion records zero rows. Verified 2026-08-11: a # 62-pair run straight after a 62-pair probe returned 429 on all 62. The backoff # is long because the window is measured in minutes, not seconds. udyam.ingestion.datagov.retry-attempts=3 udyam.ingestion.datagov.retry-backoff-ms=45000 udyam.ingestion.mandi.lookback-weeks=12 # Climate — Open-Meteo reanalysis archive. No key. The archive trails real time, # hence the lag; the baseline is fetched once per district and cached as normals. udyam.ingestion.climate.base-url=https://archive-api.open-meteo.com/v1/archive udyam.ingestion.climate.timezone=Asia/Kolkata udyam.ingestion.climate.lookback-weeks=12 udyam.ingestion.climate.lag-days=5 udyam.ingestion.climate.heat-threshold-c=40 udyam.ingestion.climate.baseline-from=1995-01-01 udyam.ingestion.climate.baseline-to=2024-12-31 # Open-Meteo meters by request cost, so the 30-year baseline call alone can trip # the minutely limit and 429 the district calls queued behind it. A rate-limited # call waits and retries; anything else fails once and is reported. udyam.ingestion.climate.retry-attempts=3 udyam.ingestion.climate.retry-backoff-ms=20000 # ── Feature store (ARCHITECTURE Phase C) ──────────────────── # Window sizes for the derivations. A level is not a feature; every one of these # turns a level into a change, a ratio or an anomaly against one of these windows. udyam.features.price-window-weeks=8 udyam.features.recent-window-weeks=4 udyam.features.climate-window-weeks=8 # Below this many same-season observations a z-score is noise, so it stays null. udyam.features.min-zscore-samples=6 logging.level.com.lincfusion.udyamlinc=INFO # ── Forecast (ARCHITECTURE R1 seam) ───────────────────────── # DETERMINISTIC until the LightGBM quantile heads exist. Both implement # ForecastStrategy, so the swap is this one property. udyam.forecast.engine=${UDYAM_FORECAST_ENGINE:DETERMINISTIC} udyam.forecast.lookback-months=6 udyam.forecast.min-months=2 # P10/P90 half-width at h=1 when every input is present; each missing input adds # to it, and it grows with the square root of the horizon. Missing inputs widen # the band, never lower the centre — a pessimistic P50 built on ignorance would # manufacture breaches. udyam.forecast.base-spread=0.18 udyam.forecast.missing-input-penalty=0.10 udyam.forecast.max-spread=0.75 udyam.forecast.max-drift=0.30 # Whether dscr_p10 may fall back to the seeder's forecast rows, which carry no # engine. True keeps the demo portfolio populated; false makes every enterprise # the forecast cannot reach report NO_FORECAST, and TW-01 skip rather than fire # on a number nothing computed. Set false before any run whose alerts are meant # to be believed. # Now false. It was true only because ledger coverage reached six enterprises of # 142, so without the fallback the console had almost no forecasts to show and 34 # trip-wires of which 5 were real. The seeder now writes five months of ledger for # every enterprise, band-coherent — a RED enterprise's monthly net genuinely sits # below its instalment — so the computed forecast is the one worth showing. udyam.forecast.trust-seeded=${UDYAM_TRUST_SEEDED_FORECASTS:false} # Furthest horizon TW-01 may act on. The six-month fan is still forecast, stored # and drawn; this bounds only what the trip-wire fires from. Past month three the # band is dominated by compounding uncertainty rather than by anything specific # to the enterprise — at the 0.75 ceiling P10 is a quarter of P50 for everyone, # so a worst-of-six rule separates nobody from anybody. udyam.forecast.tripwire-horizon=3