Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
549 KiB
JSON
1 line
549 KiB
JSON
{"repo": "aminofabian/ub", "files": {"src/main/resources/application.properties": {"d452a1d4161ea83c70cf8212b3c48e5a499096b3": {"t": 1785745398.9710095, "n": 21546, "h": "68fd8547c0bedb13", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\n# ISO country codes enabled for cloud self-serve onboarding (* / ALL = every profile\n# in region-profiles.csv). Regional catalogs are optional — tenants fall back to\n# the default catalog when none is published for that country.\napp.selfserve.countries=${APP_SELFSERVE_COUNTRIES:*}\n# Countries without card/mobile-money rails yet (stricter till story). Others still\n# get cash/card/credit until a local rail is configured. Use * / ALL for every\n# country except Kenya (M-Pesa STK).\napp.selfserve.cash-credit-only-countries=${APP_SELFSERVE_CASH_CREDIT_ONLY_COUNTRIES:UG}\n\n# Native mobile distribution (deep links, platform store URLs, bundle IDs).\napp.mobile.api-base-url=${APP_MOBILE_API_BASE_URL:https://api.kiosk.ke}\napp.mobile.deep-link-scheme=${APP_MOBILE_DEEP_LINK_SCHEME:kiosk}\napp.mobile.universal-link-scheme=${APP_MOBILE_UNIVERSAL_LINK_SCHEME:https}\napp.mobile.store-links.ios=${APP_MOBILE_STORE_LINK_IOS:}\napp.mobile.store-links.android=${APP_MOBILE_STORE_LINK_ANDROID:}\napp.mobile.platform-bundles.shopper=${APP_MOBILE_BUNDLE_SHOPPER:com.kioskke.shopper}\napp.mobile.platform-bundles.cashier=${APP_MOBILE_BUNDLE_CASHIER:com.kioskke.cashier}\napp.mobile.platform-bundles.grocery=${APP_MOBILE_BUNDLE_GROCERY:com.kioskke.grocery}\napp.mobile.platform-bundles.admin=${APP_MOBILE_BUNDLE_ADMIN:com.kioskke.admin}\napp.mobile.platform-bundles.stock=${APP_MOBILE_BUNDLE_STOCK:com.kioskke.stock}\n# Optional: GitHub Actions dispatch when a tenant requests an App Store build (repository_dispatch).\napp.mobile.publish.github-token=${APP_MOBILE_PUBLISH_GITHUB_TOKEN:}\napp.mobile.publish.github-repo=${APP_MOBILE_PUBLISH_GITHUB_REPO:}\napp.mobile.publish.event-type=${APP_MOBILE_PUBLISH_EVENT_TYPE:mobile-tenant-publish}\n# Shared secret for GitHub Actions → POST /webhooks/mobile-publish/status (openssl rand -base64 32).\napp.mobile.publish.callback-secret=${APP_MOBILE_PUBLISH_CALLBACK_SECRET:}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=${MYSQL_PASSWORD:}\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n# Allow pending lower versions (e.g. V161) when higher versions already ran — common when branches land out of sequence.\nspring.flyway.out-of-order=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=${APP_JWT_SECRET:}\napp.jwt.access-ttl-minutes=60\n# Super-admin portal tokens live longer (multi-batch promotes re-host images for a\n# long time) and slide via POST /api/v1/super-admin/auth/refresh.\napp.jwt.super-admin-access-ttl-minutes=240\n\n# HttpOnly refresh token cookie (JSON body omits refreshToken when enabled).\napp.auth.refresh-token-cookie-enabled=true\napp.auth.refresh-cookie-domain=${APP_AUTH_REFRESH_COOKIE_DOMAIN:}\napp.auth.refresh-cookie-secure=${APP_AUTH_REFRESH_COOKIE_SECURE:false}\napp.jwt.refresh-ttl-days=30\n# Revoke refresh when no activity for this many hours (sliding window, updated on refresh).\napp.auth.idle-timeout-hours=12\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.ke,www.kiosk.ke,kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\napp.security.refresh-rate-limit-per-minute=30\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Recurring balance reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Sends WhatsApp/SMS every interval-days to customers with an outstanding tab balance.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.interval-days=3\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.max-count=5\napp.credits.reminders.zone=Africa/Nairobi\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\n# SokoMind (Guide / Brain / Eye) — prefer Super Admin → Platform → SokoMind over env.\n# Env keys are fallbacks when SA has not stored a value yet.\napp.sokomind.enabled=${SOKOMIND_ENABLED:false}\napp.sokomind.guide-enabled=${SOKOMIND_GUIDE_ENABLED:true}\napp.sokomind.brain-enabled=${SOKOMIND_BRAIN_ENABLED:false}\napp.sokomind.eye-enabled=${SOKOMIND_EYE_ENABLED:false}\napp.sokomind.primary-provider=${SOKOMIND_PRIMARY_PROVIDER:openai}\napp.sokomind.default-locale=${SOKOMIND_DEFAULT_LOCALE:en-KE}\napp.sokomind.openai.api-key=${OPENAI_API_KEY:}\napp.sokomind.openai.base-url=${OPENAI_BASE_URL:}\napp.sokomind.openai.mini-model=${SOKOMIND_OPENAI_MINI_MODEL:gpt-4o-mini}\napp.sokomind.openai.smart-model=${SOKOMIND_OPENAI_SMART_MODEL:gpt-4.1}\napp.sokomind.openai.vision-model=${SOKOMIND_OPENAI_VISION_MODEL:gpt-4o}\napp.sokomind.anthropic.api-key=${ANTHROPIC_API_KEY:}\napp.sokomind.anthropic.base-url=${ANTHROPIC_BASE_URL:}\napp.sokomind.anthropic.mini-model=${SOKOMIND_ANTHROPIC_MINI_MODEL:claude-haiku-4-5-20251001}\napp.sokomind.anthropic.smart-model=${SOKOMIND_ANTHROPIC_SMART_MODEL:claude-sonnet-4-5-20250929}\napp.sokomind.deepseek.api-key=${SOKOMIND_DEEPSEEK_API_KEY:${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}}\napp.sokomind.deepseek.base-url=${SOKOMIND_DEEPSEEK_BASE_URL:https://deepseek-v31.p.rapidapi.com/}\napp.sokomind.deepseek.host=${SOKOMIND_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.sokomind.deepseek.model=${SOKOMIND_DEEPSEEK_MODEL:DeepSeek-V3-0324}\napp.sokomind.industry-compare-enabled=${SOKOMIND_INDUSTRY_COMPARE_ENABLED:false}\napp.sokomind.industry-compare-min-twins=${SOKOMIND_INDUSTRY_COMPARE_MIN_TWINS:8}\napp.sokomind.max-tool-calls-per-request=${SOKOMIND_MAX_TOOL_CALLS:8}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# Meta webhook: Callback URL = ${API_PUBLIC_BASE_URL}/webhooks/whatsapp (same verify token in Meta dashboard).\napp.messaging.meta-whatsapp.webhook-verify-token=${WHATSAPP_META_WEBHOOK_VERIFY_TOKEN:}\napp.messaging.meta-whatsapp.app-secret=${WHATSAPP_META_APP_SECRET:}\n# SMS: none (log only) | africas_talking | sozuri | textsms\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\napp.messaging.sms.sozuri-project=${SOZURI_PROJECT:}\napp.messaging.sms.sozuri-api-key=${SOZURI_API_KEY:}\napp.messaging.sms.sozuri-from=${SOZURI_FROM:Sozuri}\napp.messaging.sms.sozuri-type=${SOZURI_TYPE:transactional}\napp.messaging.sms.sozuri-api-url=${SOZURI_API_URL:https://sozuri.net/api/v1/messaging}\n# When SMS is not configured, claim still works by returning the OTP in the JSON (local/dev).\n# Set false in production once Super Admin → Platform → Integrations has Sozuri/TextSMS.\napp.supplier-portal.claim.return-otp-when-stubbed=${APP_SUPPLIER_PORTAL_RETURN_OTP:true}\n# Sozuri callbacks: {API_PUBLIC_BASE_URL}/webhooks/sozuri/inbox and /webhooks/sozuri/delivery\napp.messaging.sms.textsms-partner-id=${TEXTSMS_PARTNER_ID:}\napp.messaging.sms.textsms-api-key=${TEXTSMS_API_KEY:}\napp.messaging.sms.textsms-shortcode=${TEXTSMS_SHORTCODE:}\napp.messaging.sms.textsms-api-url=${TEXTSMS_API_URL:https://sms.textsms.co.ke/api/services/sendsms/}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\n\n# Daily stock audit — random sample of products sold yesterday (branch-scoped manifest).\n# Per-business override: inventory.stocktake.dailyAuditSampleSize (Business settings → Stock take).\napp.inventory.daily-stock-audit.enabled=true\napp.inventory.daily-stock-audit.cron=0 0 6 * * *\napp.inventory.daily-stock-audit.zone=Africa/Nairobi\napp.inventory.daily-stock-audit.sample-size=25\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 4 — async global catalog adopt / promote jobs.\napp.global-catalog.jobs.worker.enabled=true\napp.global-catalog.jobs.worker.poll-interval-ms=3000\napp.global-catalog.jobs.worker.initial-delay-ms=5000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Custom domains — optional env fallback only. Prefer Super Admin → Platform → Domains.\napp.integrations.vercel.token=${VERCEL_TOKEN:}\napp.integrations.vercel.team-id=${VERCEL_TEAM_ID:}\napp.integrations.vercel.project-id=${VERCEL_PROJECT_ID:}\napp.integrations.vercel.api-base-url=${VERCEL_API_BASE_URL:https://api.vercel.com}\n\napp.integrations.hostafrica.api-key=${HOSTAFRICA_API_KEY:}\napp.integrations.hostafrica.api-base-url=${HOSTAFRICA_API_BASE_URL:https://api.hostafrica.com}\napp.integrations.hostafrica.currency=${HOSTAFRICA_CURRENCY:KES}\napp.integrations.hostafrica.billing-stub-enabled=${HOSTAFRICA_BILLING_STUB_ENABLED:true}\napp.integrations.hostafrica.kenyan-tlds=${HOSTAFRICA_KENYAN_TLDS:co.ke,or.ke,me.ke,sc.ke,ac.ke,go.ke,ke}\n\n# Scheduler delay defaults (enable sync from Super Admin Domains page).\napp.integrations.domains.sync-fixed-delay-ms=${DOMAIN_ORDER_SYNC_FIXED_DELAY_MS:60000}\napp.integrations.domains.sync-initial-delay-ms=${DOMAIN_ORDER_SYNC_INITIAL_DELAY_MS:20000}\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "8c28ac236c810780e0932e45d225c18602bd55f1": {"t": 1785752686.114063, "n": 21546, "h": "68fd8547c0bedb13", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\n# ISO country codes enabled for cloud self-serve onboarding (* / ALL = every profile\n# in region-profiles.csv). Regional catalogs are optional — tenants fall back to\n# the default catalog when none is published for that country.\napp.selfserve.countries=${APP_SELFSERVE_COUNTRIES:*}\n# Countries without card/mobile-money rails yet (stricter till story). Others still\n# get cash/card/credit until a local rail is configured. Use * / ALL for every\n# country except Kenya (M-Pesa STK).\napp.selfserve.cash-credit-only-countries=${APP_SELFSERVE_CASH_CREDIT_ONLY_COUNTRIES:UG}\n\n# Native mobile distribution (deep links, platform store URLs, bundle IDs).\napp.mobile.api-base-url=${APP_MOBILE_API_BASE_URL:https://api.kiosk.ke}\napp.mobile.deep-link-scheme=${APP_MOBILE_DEEP_LINK_SCHEME:kiosk}\napp.mobile.universal-link-scheme=${APP_MOBILE_UNIVERSAL_LINK_SCHEME:https}\napp.mobile.store-links.ios=${APP_MOBILE_STORE_LINK_IOS:}\napp.mobile.store-links.android=${APP_MOBILE_STORE_LINK_ANDROID:}\napp.mobile.platform-bundles.shopper=${APP_MOBILE_BUNDLE_SHOPPER:com.kioskke.shopper}\napp.mobile.platform-bundles.cashier=${APP_MOBILE_BUNDLE_CASHIER:com.kioskke.cashier}\napp.mobile.platform-bundles.grocery=${APP_MOBILE_BUNDLE_GROCERY:com.kioskke.grocery}\napp.mobile.platform-bundles.admin=${APP_MOBILE_BUNDLE_ADMIN:com.kioskke.admin}\napp.mobile.platform-bundles.stock=${APP_MOBILE_BUNDLE_STOCK:com.kioskke.stock}\n# Optional: GitHub Actions dispatch when a tenant requests an App Store build (repository_dispatch).\napp.mobile.publish.github-token=${APP_MOBILE_PUBLISH_GITHUB_TOKEN:}\napp.mobile.publish.github-repo=${APP_MOBILE_PUBLISH_GITHUB_REPO:}\napp.mobile.publish.event-type=${APP_MOBILE_PUBLISH_EVENT_TYPE:mobile-tenant-publish}\n# Shared secret for GitHub Actions → POST /webhooks/mobile-publish/status (openssl rand -base64 32).\napp.mobile.publish.callback-secret=${APP_MOBILE_PUBLISH_CALLBACK_SECRET:}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=${MYSQL_PASSWORD:}\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n# Allow pending lower versions (e.g. V161) when higher versions already ran — common when branches land out of sequence.\nspring.flyway.out-of-order=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=${APP_JWT_SECRET:}\napp.jwt.access-ttl-minutes=60\n# Super-admin portal tokens live longer (multi-batch promotes re-host images for a\n# long time) and slide via POST /api/v1/super-admin/auth/refresh.\napp.jwt.super-admin-access-ttl-minutes=240\n\n# HttpOnly refresh token cookie (JSON body omits refreshToken when enabled).\napp.auth.refresh-token-cookie-enabled=true\napp.auth.refresh-cookie-domain=${APP_AUTH_REFRESH_COOKIE_DOMAIN:}\napp.auth.refresh-cookie-secure=${APP_AUTH_REFRESH_COOKIE_SECURE:false}\napp.jwt.refresh-ttl-days=30\n# Revoke refresh when no activity for this many hours (sliding window, updated on refresh).\napp.auth.idle-timeout-hours=12\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.ke,www.kiosk.ke,kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\napp.security.refresh-rate-limit-per-minute=30\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Recurring balance reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Sends WhatsApp/SMS every interval-days to customers with an outstanding tab balance.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.interval-days=3\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.max-count=5\napp.credits.reminders.zone=Africa/Nairobi\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\n# SokoMind (Guide / Brain / Eye) — prefer Super Admin → Platform → SokoMind over env.\n# Env keys are fallbacks when SA has not stored a value yet.\napp.sokomind.enabled=${SOKOMIND_ENABLED:false}\napp.sokomind.guide-enabled=${SOKOMIND_GUIDE_ENABLED:true}\napp.sokomind.brain-enabled=${SOKOMIND_BRAIN_ENABLED:false}\napp.sokomind.eye-enabled=${SOKOMIND_EYE_ENABLED:false}\napp.sokomind.primary-provider=${SOKOMIND_PRIMARY_PROVIDER:openai}\napp.sokomind.default-locale=${SOKOMIND_DEFAULT_LOCALE:en-KE}\napp.sokomind.openai.api-key=${OPENAI_API_KEY:}\napp.sokomind.openai.base-url=${OPENAI_BASE_URL:}\napp.sokomind.openai.mini-model=${SOKOMIND_OPENAI_MINI_MODEL:gpt-4o-mini}\napp.sokomind.openai.smart-model=${SOKOMIND_OPENAI_SMART_MODEL:gpt-4.1}\napp.sokomind.openai.vision-model=${SOKOMIND_OPENAI_VISION_MODEL:gpt-4o}\napp.sokomind.anthropic.api-key=${ANTHROPIC_API_KEY:}\napp.sokomind.anthropic.base-url=${ANTHROPIC_BASE_URL:}\napp.sokomind.anthropic.mini-model=${SOKOMIND_ANTHROPIC_MINI_MODEL:claude-haiku-4-5-20251001}\napp.sokomind.anthropic.smart-model=${SOKOMIND_ANTHROPIC_SMART_MODEL:claude-sonnet-4-5-20250929}\napp.sokomind.deepseek.api-key=${SOKOMIND_DEEPSEEK_API_KEY:${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}}\napp.sokomind.deepseek.base-url=${SOKOMIND_DEEPSEEK_BASE_URL:https://deepseek-v31.p.rapidapi.com/}\napp.sokomind.deepseek.host=${SOKOMIND_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.sokomind.deepseek.model=${SOKOMIND_DEEPSEEK_MODEL:DeepSeek-V3-0324}\napp.sokomind.industry-compare-enabled=${SOKOMIND_INDUSTRY_COMPARE_ENABLED:false}\napp.sokomind.industry-compare-min-twins=${SOKOMIND_INDUSTRY_COMPARE_MIN_TWINS:8}\napp.sokomind.max-tool-calls-per-request=${SOKOMIND_MAX_TOOL_CALLS:8}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# Meta webhook: Callback URL = ${API_PUBLIC_BASE_URL}/webhooks/whatsapp (same verify token in Meta dashboard).\napp.messaging.meta-whatsapp.webhook-verify-token=${WHATSAPP_META_WEBHOOK_VERIFY_TOKEN:}\napp.messaging.meta-whatsapp.app-secret=${WHATSAPP_META_APP_SECRET:}\n# SMS: none (log only) | africas_talking | sozuri | textsms\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\napp.messaging.sms.sozuri-project=${SOZURI_PROJECT:}\napp.messaging.sms.sozuri-api-key=${SOZURI_API_KEY:}\napp.messaging.sms.sozuri-from=${SOZURI_FROM:Sozuri}\napp.messaging.sms.sozuri-type=${SOZURI_TYPE:transactional}\napp.messaging.sms.sozuri-api-url=${SOZURI_API_URL:https://sozuri.net/api/v1/messaging}\n# When SMS is not configured, claim still works by returning the OTP in the JSON (local/dev).\n# Set false in production once Super Admin → Platform → Integrations has Sozuri/TextSMS.\napp.supplier-portal.claim.return-otp-when-stubbed=${APP_SUPPLIER_PORTAL_RETURN_OTP:true}\n# Sozuri callbacks: {API_PUBLIC_BASE_URL}/webhooks/sozuri/inbox and /webhooks/sozuri/delivery\napp.messaging.sms.textsms-partner-id=${TEXTSMS_PARTNER_ID:}\napp.messaging.sms.textsms-api-key=${TEXTSMS_API_KEY:}\napp.messaging.sms.textsms-shortcode=${TEXTSMS_SHORTCODE:}\napp.messaging.sms.textsms-api-url=${TEXTSMS_API_URL:https://sms.textsms.co.ke/api/services/sendsms/}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\n\n# Daily stock audit — random sample of products sold yesterday (branch-scoped manifest).\n# Per-business override: inventory.stocktake.dailyAuditSampleSize (Business settings → Stock take).\napp.inventory.daily-stock-audit.enabled=true\napp.inventory.daily-stock-audit.cron=0 0 6 * * *\napp.inventory.daily-stock-audit.zone=Africa/Nairobi\napp.inventory.daily-stock-audit.sample-size=25\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 4 — async global catalog adopt / promote jobs.\napp.global-catalog.jobs.worker.enabled=true\napp.global-catalog.jobs.worker.poll-interval-ms=3000\napp.global-catalog.jobs.worker.initial-delay-ms=5000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Custom domains — optional env fallback only. Prefer Super Admin → Platform → Domains.\napp.integrations.vercel.token=${VERCEL_TOKEN:}\napp.integrations.vercel.team-id=${VERCEL_TEAM_ID:}\napp.integrations.vercel.project-id=${VERCEL_PROJECT_ID:}\napp.integrations.vercel.api-base-url=${VERCEL_API_BASE_URL:https://api.vercel.com}\n\napp.integrations.hostafrica.api-key=${HOSTAFRICA_API_KEY:}\napp.integrations.hostafrica.api-base-url=${HOSTAFRICA_API_BASE_URL:https://api.hostafrica.com}\napp.integrations.hostafrica.currency=${HOSTAFRICA_CURRENCY:KES}\napp.integrations.hostafrica.billing-stub-enabled=${HOSTAFRICA_BILLING_STUB_ENABLED:true}\napp.integrations.hostafrica.kenyan-tlds=${HOSTAFRICA_KENYAN_TLDS:co.ke,or.ke,me.ke,sc.ke,ac.ke,go.ke,ke}\n\n# Scheduler delay defaults (enable sync from Super Admin Domains page).\napp.integrations.domains.sync-fixed-delay-ms=${DOMAIN_ORDER_SYNC_FIXED_DELAY_MS:60000}\napp.integrations.domains.sync-initial-delay-ms=${DOMAIN_ORDER_SYNC_INITIAL_DELAY_MS:20000}\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "0423b54068b648091d3b29dda43d0d0490b56bc9": {"t": 1785752689.5225768, "n": 21354, "h": "37ca599b1100092c", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\n# ISO country codes enabled for cloud self-serve onboarding (* / ALL = every profile\n# in region-profiles.csv). Regional catalogs are optional — tenants fall back to\n# the default catalog when none is published for that country.\napp.selfserve.countries=${APP_SELFSERVE_COUNTRIES:*}\n# Countries without card/mobile-money rails yet (stricter till story). Others still\n# get cash/card/credit until a local rail is configured. Use * / ALL for every\n# country except Kenya (M-Pesa STK).\napp.selfserve.cash-credit-only-countries=${APP_SELFSERVE_CASH_CREDIT_ONLY_COUNTRIES:UG}\n\n# Native mobile distribution (deep links, platform store URLs, bundle IDs).\napp.mobile.api-base-url=${APP_MOBILE_API_BASE_URL:https://api.kiosk.ke}\napp.mobile.deep-link-scheme=${APP_MOBILE_DEEP_LINK_SCHEME:kiosk}\napp.mobile.universal-link-scheme=${APP_MOBILE_UNIVERSAL_LINK_SCHEME:https}\napp.mobile.store-links.ios=${APP_MOBILE_STORE_LINK_IOS:}\napp.mobile.store-links.android=${APP_MOBILE_STORE_LINK_ANDROID:}\napp.mobile.platform-bundles.shopper=${APP_MOBILE_BUNDLE_SHOPPER:com.kioskke.shopper}\napp.mobile.platform-bundles.cashier=${APP_MOBILE_BUNDLE_CASHIER:com.kioskke.cashier}\napp.mobile.platform-bundles.grocery=${APP_MOBILE_BUNDLE_GROCERY:com.kioskke.grocery}\napp.mobile.platform-bundles.admin=${APP_MOBILE_BUNDLE_ADMIN:com.kioskke.admin}\napp.mobile.platform-bundles.stock=${APP_MOBILE_BUNDLE_STOCK:com.kioskke.stock}\n# Optional: GitHub Actions dispatch when a tenant requests an App Store build (repository_dispatch).\napp.mobile.publish.github-token=${APP_MOBILE_PUBLISH_GITHUB_TOKEN:}\napp.mobile.publish.github-repo=${APP_MOBILE_PUBLISH_GITHUB_REPO:}\napp.mobile.publish.event-type=${APP_MOBILE_PUBLISH_EVENT_TYPE:mobile-tenant-publish}\n# Shared secret for GitHub Actions → POST /webhooks/mobile-publish/status (openssl rand -base64 32).\napp.mobile.publish.callback-secret=${APP_MOBILE_PUBLISH_CALLBACK_SECRET:}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=${MYSQL_PASSWORD:}\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n# Allow pending lower versions (e.g. V161) when higher versions already ran — common when branches land out of sequence.\nspring.flyway.out-of-order=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=${APP_JWT_SECRET:}\napp.jwt.access-ttl-minutes=60\n\n# HttpOnly refresh token cookie (JSON body omits refreshToken when enabled).\napp.auth.refresh-token-cookie-enabled=true\napp.auth.refresh-cookie-domain=${APP_AUTH_REFRESH_COOKIE_DOMAIN:}\napp.auth.refresh-cookie-secure=${APP_AUTH_REFRESH_COOKIE_SECURE:false}\napp.jwt.refresh-ttl-days=30\n# Revoke refresh when no activity for this many hours (sliding window, updated on refresh).\napp.auth.idle-timeout-hours=12\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.ke,www.kiosk.ke,kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\napp.security.refresh-rate-limit-per-minute=30\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Recurring balance reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Sends WhatsApp/SMS every interval-days to customers with an outstanding tab balance.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.interval-days=3\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.max-count=5\napp.credits.reminders.zone=Africa/Nairobi\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\n# SokoMind (Guide / Brain / Eye) — prefer Super Admin → Platform → SokoMind over env.\n# Env keys are fallbacks when SA has not stored a value yet.\napp.sokomind.enabled=${SOKOMIND_ENABLED:false}\napp.sokomind.guide-enabled=${SOKOMIND_GUIDE_ENABLED:true}\napp.sokomind.brain-enabled=${SOKOMIND_BRAIN_ENABLED:false}\napp.sokomind.eye-enabled=${SOKOMIND_EYE_ENABLED:false}\napp.sokomind.primary-provider=${SOKOMIND_PRIMARY_PROVIDER:openai}\napp.sokomind.default-locale=${SOKOMIND_DEFAULT_LOCALE:en-KE}\napp.sokomind.openai.api-key=${OPENAI_API_KEY:}\napp.sokomind.openai.base-url=${OPENAI_BASE_URL:}\napp.sokomind.openai.mini-model=${SOKOMIND_OPENAI_MINI_MODEL:gpt-4o-mini}\napp.sokomind.openai.smart-model=${SOKOMIND_OPENAI_SMART_MODEL:gpt-4.1}\napp.sokomind.openai.vision-model=${SOKOMIND_OPENAI_VISION_MODEL:gpt-4o}\napp.sokomind.anthropic.api-key=${ANTHROPIC_API_KEY:}\napp.sokomind.anthropic.base-url=${ANTHROPIC_BASE_URL:}\napp.sokomind.anthropic.mini-model=${SOKOMIND_ANTHROPIC_MINI_MODEL:claude-haiku-4-5-20251001}\napp.sokomind.anthropic.smart-model=${SOKOMIND_ANTHROPIC_SMART_MODEL:claude-sonnet-4-5-20250929}\napp.sokomind.deepseek.api-key=${SOKOMIND_DEEPSEEK_API_KEY:${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}}\napp.sokomind.deepseek.base-url=${SOKOMIND_DEEPSEEK_BASE_URL:https://deepseek-v31.p.rapidapi.com/}\napp.sokomind.deepseek.host=${SOKOMIND_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.sokomind.deepseek.model=${SOKOMIND_DEEPSEEK_MODEL:DeepSeek-V3-0324}\napp.sokomind.industry-compare-enabled=${SOKOMIND_INDUSTRY_COMPARE_ENABLED:false}\napp.sokomind.industry-compare-min-twins=${SOKOMIND_INDUSTRY_COMPARE_MIN_TWINS:8}\napp.sokomind.max-tool-calls-per-request=${SOKOMIND_MAX_TOOL_CALLS:8}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# Meta webhook: Callback URL = ${API_PUBLIC_BASE_URL}/webhooks/whatsapp (same verify token in Meta dashboard).\napp.messaging.meta-whatsapp.webhook-verify-token=${WHATSAPP_META_WEBHOOK_VERIFY_TOKEN:}\napp.messaging.meta-whatsapp.app-secret=${WHATSAPP_META_APP_SECRET:}\n# SMS: none (log only) | africas_talking | sozuri | textsms\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\napp.messaging.sms.sozuri-project=${SOZURI_PROJECT:}\napp.messaging.sms.sozuri-api-key=${SOZURI_API_KEY:}\napp.messaging.sms.sozuri-from=${SOZURI_FROM:Sozuri}\napp.messaging.sms.sozuri-type=${SOZURI_TYPE:transactional}\napp.messaging.sms.sozuri-api-url=${SOZURI_API_URL:https://sozuri.net/api/v1/messaging}\n# When SMS is not configured, claim still works by returning the OTP in the JSON (local/dev).\n# Set false in production once Super Admin → Platform → Integrations has Sozuri/TextSMS.\napp.supplier-portal.claim.return-otp-when-stubbed=${APP_SUPPLIER_PORTAL_RETURN_OTP:true}\n# Sozuri callbacks: {API_PUBLIC_BASE_URL}/webhooks/sozuri/inbox and /webhooks/sozuri/delivery\napp.messaging.sms.textsms-partner-id=${TEXTSMS_PARTNER_ID:}\napp.messaging.sms.textsms-api-key=${TEXTSMS_API_KEY:}\napp.messaging.sms.textsms-shortcode=${TEXTSMS_SHORTCODE:}\napp.messaging.sms.textsms-api-url=${TEXTSMS_API_URL:https://sms.textsms.co.ke/api/services/sendsms/}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\n\n# Daily stock audit — random sample of products sold yesterday (branch-scoped manifest).\n# Per-business override: inventory.stocktake.dailyAuditSampleSize (Business settings → Stock take).\napp.inventory.daily-stock-audit.enabled=true\napp.inventory.daily-stock-audit.cron=0 0 6 * * *\napp.inventory.daily-stock-audit.zone=Africa/Nairobi\napp.inventory.daily-stock-audit.sample-size=25\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 4 — async global catalog adopt / promote jobs.\napp.global-catalog.jobs.worker.enabled=true\napp.global-catalog.jobs.worker.poll-interval-ms=3000\napp.global-catalog.jobs.worker.initial-delay-ms=5000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Custom domains — optional env fallback only. Prefer Super Admin → Platform → Domains.\napp.integrations.vercel.token=${VERCEL_TOKEN:}\napp.integrations.vercel.team-id=${VERCEL_TEAM_ID:}\napp.integrations.vercel.project-id=${VERCEL_PROJECT_ID:}\napp.integrations.vercel.api-base-url=${VERCEL_API_BASE_URL:https://api.vercel.com}\n\napp.integrations.hostafrica.api-key=${HOSTAFRICA_API_KEY:}\napp.integrations.hostafrica.api-base-url=${HOSTAFRICA_API_BASE_URL:https://api.hostafrica.com}\napp.integrations.hostafrica.currency=${HOSTAFRICA_CURRENCY:KES}\napp.integrations.hostafrica.billing-stub-enabled=${HOSTAFRICA_BILLING_STUB_ENABLED:true}\napp.integrations.hostafrica.kenyan-tlds=${HOSTAFRICA_KENYAN_TLDS:co.ke,or.ke,me.ke,sc.ke,ac.ke,go.ke,ke}\n\n# Scheduler delay defaults (enable sync from Super Admin Domains page).\napp.integrations.domains.sync-fixed-delay-ms=${DOMAIN_ORDER_SYNC_FIXED_DELAY_MS:60000}\napp.integrations.domains.sync-initial-delay-ms=${DOMAIN_ORDER_SYNC_INITIAL_DELAY_MS:20000}\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "65b5fddd22075951611ec5c5f789d5a198326700": {"t": 1785752692.4591317, "n": 21246, "h": "6c1ef7f8420b5659", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\n# Comma-separated ISO country codes enabled for cloud self-serve onboarding.\n# A country must also have a published regional catalog (see RegionDefaults).\n# UG is enabled with cash/credit-only exception (see cash-credit-only-countries).\napp.selfserve.countries=${APP_SELFSERVE_COUNTRIES:KE,UG}\n# Countries without a local mobile-money rail yet (explicit product exception).\napp.selfserve.cash-credit-only-countries=${APP_SELFSERVE_CASH_CREDIT_ONLY_COUNTRIES:UG}\n\n# Native mobile distribution (deep links, platform store URLs, bundle IDs).\napp.mobile.api-base-url=${APP_MOBILE_API_BASE_URL:https://api.kiosk.ke}\napp.mobile.deep-link-scheme=${APP_MOBILE_DEEP_LINK_SCHEME:kiosk}\napp.mobile.universal-link-scheme=${APP_MOBILE_UNIVERSAL_LINK_SCHEME:https}\napp.mobile.store-links.ios=${APP_MOBILE_STORE_LINK_IOS:}\napp.mobile.store-links.android=${APP_MOBILE_STORE_LINK_ANDROID:}\napp.mobile.platform-bundles.shopper=${APP_MOBILE_BUNDLE_SHOPPER:com.kioskke.shopper}\napp.mobile.platform-bundles.cashier=${APP_MOBILE_BUNDLE_CASHIER:com.kioskke.cashier}\napp.mobile.platform-bundles.grocery=${APP_MOBILE_BUNDLE_GROCERY:com.kioskke.grocery}\napp.mobile.platform-bundles.admin=${APP_MOBILE_BUNDLE_ADMIN:com.kioskke.admin}\napp.mobile.platform-bundles.stock=${APP_MOBILE_BUNDLE_STOCK:com.kioskke.stock}\n# Optional: GitHub Actions dispatch when a tenant requests an App Store build (repository_dispatch).\napp.mobile.publish.github-token=${APP_MOBILE_PUBLISH_GITHUB_TOKEN:}\napp.mobile.publish.github-repo=${APP_MOBILE_PUBLISH_GITHUB_REPO:}\napp.mobile.publish.event-type=${APP_MOBILE_PUBLISH_EVENT_TYPE:mobile-tenant-publish}\n# Shared secret for GitHub Actions → POST /webhooks/mobile-publish/status (openssl rand -base64 32).\napp.mobile.publish.callback-secret=${APP_MOBILE_PUBLISH_CALLBACK_SECRET:}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=${MYSQL_PASSWORD:}\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n# Allow pending lower versions (e.g. V161) when higher versions already ran — common when branches land out of sequence.\nspring.flyway.out-of-order=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=${APP_JWT_SECRET:}\napp.jwt.access-ttl-minutes=60\n\n# HttpOnly refresh token cookie (JSON body omits refreshToken when enabled).\napp.auth.refresh-token-cookie-enabled=true\napp.auth.refresh-cookie-domain=${APP_AUTH_REFRESH_COOKIE_DOMAIN:}\napp.auth.refresh-cookie-secure=${APP_AUTH_REFRESH_COOKIE_SECURE:false}\napp.jwt.refresh-ttl-days=30\n# Revoke refresh when no activity for this many hours (sliding window, updated on refresh).\napp.auth.idle-timeout-hours=12\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.ke,www.kiosk.ke,kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\napp.security.refresh-rate-limit-per-minute=30\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Recurring balance reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Sends WhatsApp/SMS every interval-days to customers with an outstanding tab balance.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.interval-days=3\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.max-count=5\napp.credits.reminders.zone=Africa/Nairobi\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\n# SokoMind (Guide / Brain / Eye) — prefer Super Admin → Platform → SokoMind over env.\n# Env keys are fallbacks when SA has not stored a value yet.\napp.sokomind.enabled=${SOKOMIND_ENABLED:false}\napp.sokomind.guide-enabled=${SOKOMIND_GUIDE_ENABLED:true}\napp.sokomind.brain-enabled=${SOKOMIND_BRAIN_ENABLED:false}\napp.sokomind.eye-enabled=${SOKOMIND_EYE_ENABLED:false}\napp.sokomind.primary-provider=${SOKOMIND_PRIMARY_PROVIDER:openai}\napp.sokomind.default-locale=${SOKOMIND_DEFAULT_LOCALE:en-KE}\napp.sokomind.openai.api-key=${OPENAI_API_KEY:}\napp.sokomind.openai.base-url=${OPENAI_BASE_URL:}\napp.sokomind.openai.mini-model=${SOKOMIND_OPENAI_MINI_MODEL:gpt-4o-mini}\napp.sokomind.openai.smart-model=${SOKOMIND_OPENAI_SMART_MODEL:gpt-4.1}\napp.sokomind.openai.vision-model=${SOKOMIND_OPENAI_VISION_MODEL:gpt-4o}\napp.sokomind.anthropic.api-key=${ANTHROPIC_API_KEY:}\napp.sokomind.anthropic.base-url=${ANTHROPIC_BASE_URL:}\napp.sokomind.anthropic.mini-model=${SOKOMIND_ANTHROPIC_MINI_MODEL:claude-haiku-4-5-20251001}\napp.sokomind.anthropic.smart-model=${SOKOMIND_ANTHROPIC_SMART_MODEL:claude-sonnet-4-5-20250929}\napp.sokomind.deepseek.api-key=${SOKOMIND_DEEPSEEK_API_KEY:${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}}\napp.sokomind.deepseek.base-url=${SOKOMIND_DEEPSEEK_BASE_URL:https://deepseek-v31.p.rapidapi.com/}\napp.sokomind.deepseek.host=${SOKOMIND_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.sokomind.deepseek.model=${SOKOMIND_DEEPSEEK_MODEL:DeepSeek-V3-0324}\napp.sokomind.industry-compare-enabled=${SOKOMIND_INDUSTRY_COMPARE_ENABLED:false}\napp.sokomind.industry-compare-min-twins=${SOKOMIND_INDUSTRY_COMPARE_MIN_TWINS:8}\napp.sokomind.max-tool-calls-per-request=${SOKOMIND_MAX_TOOL_CALLS:8}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# Meta webhook: Callback URL = ${API_PUBLIC_BASE_URL}/webhooks/whatsapp (same verify token in Meta dashboard).\napp.messaging.meta-whatsapp.webhook-verify-token=${WHATSAPP_META_WEBHOOK_VERIFY_TOKEN:}\napp.messaging.meta-whatsapp.app-secret=${WHATSAPP_META_APP_SECRET:}\n# SMS: none (log only) | africas_talking | sozuri | textsms\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\napp.messaging.sms.sozuri-project=${SOZURI_PROJECT:}\napp.messaging.sms.sozuri-api-key=${SOZURI_API_KEY:}\napp.messaging.sms.sozuri-from=${SOZURI_FROM:Sozuri}\napp.messaging.sms.sozuri-type=${SOZURI_TYPE:transactional}\napp.messaging.sms.sozuri-api-url=${SOZURI_API_URL:https://sozuri.net/api/v1/messaging}\n# When SMS is not configured, claim still works by returning the OTP in the JSON (local/dev).\n# Set false in production once Super Admin → Platform → Integrations has Sozuri/TextSMS.\napp.supplier-portal.claim.return-otp-when-stubbed=${APP_SUPPLIER_PORTAL_RETURN_OTP:true}\n# Sozuri callbacks: {API_PUBLIC_BASE_URL}/webhooks/sozuri/inbox and /webhooks/sozuri/delivery\napp.messaging.sms.textsms-partner-id=${TEXTSMS_PARTNER_ID:}\napp.messaging.sms.textsms-api-key=${TEXTSMS_API_KEY:}\napp.messaging.sms.textsms-shortcode=${TEXTSMS_SHORTCODE:}\napp.messaging.sms.textsms-api-url=${TEXTSMS_API_URL:https://sms.textsms.co.ke/api/services/sendsms/}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\n\n# Daily stock audit — random sample of products sold yesterday (branch-scoped manifest).\n# Per-business override: inventory.stocktake.dailyAuditSampleSize (Business settings → Stock take).\napp.inventory.daily-stock-audit.enabled=true\napp.inventory.daily-stock-audit.cron=0 0 6 * * *\napp.inventory.daily-stock-audit.zone=Africa/Nairobi\napp.inventory.daily-stock-audit.sample-size=25\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 4 — async global catalog adopt / promote jobs.\napp.global-catalog.jobs.worker.enabled=true\napp.global-catalog.jobs.worker.poll-interval-ms=3000\napp.global-catalog.jobs.worker.initial-delay-ms=5000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Custom domains — optional env fallback only. Prefer Super Admin → Platform → Domains.\napp.integrations.vercel.token=${VERCEL_TOKEN:}\napp.integrations.vercel.team-id=${VERCEL_TEAM_ID:}\napp.integrations.vercel.project-id=${VERCEL_PROJECT_ID:}\napp.integrations.vercel.api-base-url=${VERCEL_API_BASE_URL:https://api.vercel.com}\n\napp.integrations.hostafrica.api-key=${HOSTAFRICA_API_KEY:}\napp.integrations.hostafrica.api-base-url=${HOSTAFRICA_API_BASE_URL:https://api.hostafrica.com}\napp.integrations.hostafrica.currency=${HOSTAFRICA_CURRENCY:KES}\napp.integrations.hostafrica.billing-stub-enabled=${HOSTAFRICA_BILLING_STUB_ENABLED:true}\napp.integrations.hostafrica.kenyan-tlds=${HOSTAFRICA_KENYAN_TLDS:co.ke,or.ke,me.ke,sc.ke,ac.ke,go.ke,ke}\n\n# Scheduler delay defaults (enable sync from Super Admin Domains page).\napp.integrations.domains.sync-fixed-delay-ms=${DOMAIN_ORDER_SYNC_FIXED_DELAY_MS:60000}\napp.integrations.domains.sync-initial-delay-ms=${DOMAIN_ORDER_SYNC_INITIAL_DELAY_MS:20000}\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "2147a16343ad0c606e15684e98cb097ba3e49be9": {"t": 1785752694.2357323, "n": 20255, "h": "8b907195f5fe7c29", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\n# Comma-separated ISO country codes enabled for cloud self-serve onboarding.\n# A country must also have a published regional catalog (see RegionDefaults).\n# UG is enabled with cash/credit-only exception (see cash-credit-only-countries).\napp.selfserve.countries=${APP_SELFSERVE_COUNTRIES:KE,UG}\n# Countries without a local mobile-money rail yet (explicit product exception).\napp.selfserve.cash-credit-only-countries=${APP_SELFSERVE_CASH_CREDIT_ONLY_COUNTRIES:UG}\n\n# Native mobile distribution (deep links, platform store URLs, bundle IDs).\napp.mobile.api-base-url=${APP_MOBILE_API_BASE_URL:https://api.kiosk.ke}\napp.mobile.deep-link-scheme=${APP_MOBILE_DEEP_LINK_SCHEME:kiosk}\napp.mobile.universal-link-scheme=${APP_MOBILE_UNIVERSAL_LINK_SCHEME:https}\napp.mobile.store-links.ios=${APP_MOBILE_STORE_LINK_IOS:}\napp.mobile.store-links.android=${APP_MOBILE_STORE_LINK_ANDROID:}\napp.mobile.platform-bundles.shopper=${APP_MOBILE_BUNDLE_SHOPPER:com.kioskke.shopper}\napp.mobile.platform-bundles.cashier=${APP_MOBILE_BUNDLE_CASHIER:com.kioskke.cashier}\napp.mobile.platform-bundles.grocery=${APP_MOBILE_BUNDLE_GROCERY:com.kioskke.grocery}\napp.mobile.platform-bundles.admin=${APP_MOBILE_BUNDLE_ADMIN:com.kioskke.admin}\napp.mobile.platform-bundles.stock=${APP_MOBILE_BUNDLE_STOCK:com.kioskke.stock}\n# Optional: GitHub Actions dispatch when a tenant requests an App Store build (repository_dispatch).\napp.mobile.publish.github-token=${APP_MOBILE_PUBLISH_GITHUB_TOKEN:}\napp.mobile.publish.github-repo=${APP_MOBILE_PUBLISH_GITHUB_REPO:}\napp.mobile.publish.event-type=${APP_MOBILE_PUBLISH_EVENT_TYPE:mobile-tenant-publish}\n# Shared secret for GitHub Actions → POST /webhooks/mobile-publish/status (openssl rand -base64 32).\napp.mobile.publish.callback-secret=${APP_MOBILE_PUBLISH_CALLBACK_SECRET:}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=${MYSQL_PASSWORD:}\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n# Allow pending lower versions (e.g. V161) when higher versions already ran — common when branches land out of sequence.\nspring.flyway.out-of-order=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=${APP_JWT_SECRET:}\napp.jwt.access-ttl-minutes=60\n\n# HttpOnly refresh token cookie (JSON body omits refreshToken when enabled).\napp.auth.refresh-token-cookie-enabled=true\napp.auth.refresh-cookie-domain=${APP_AUTH_REFRESH_COOKIE_DOMAIN:}\napp.auth.refresh-cookie-secure=${APP_AUTH_REFRESH_COOKIE_SECURE:false}\napp.jwt.refresh-ttl-days=30\n# Revoke refresh when no activity for this many hours (sliding window, updated on refresh).\napp.auth.idle-timeout-hours=12\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.ke,www.kiosk.ke,kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\napp.security.refresh-rate-limit-per-minute=30\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Recurring balance reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Sends WhatsApp/SMS every interval-days to customers with an outstanding tab balance.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.interval-days=3\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.max-count=5\napp.credits.reminders.zone=Africa/Nairobi\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\n# SokoMind (Guide / Brain / Eye) — prefer Super Admin → Platform → SokoMind over env.\n# Env keys are fallbacks when SA has not stored a value yet.\napp.sokomind.enabled=${SOKOMIND_ENABLED:false}\napp.sokomind.guide-enabled=${SOKOMIND_GUIDE_ENABLED:true}\napp.sokomind.brain-enabled=${SOKOMIND_BRAIN_ENABLED:false}\napp.sokomind.eye-enabled=${SOKOMIND_EYE_ENABLED:false}\napp.sokomind.primary-provider=${SOKOMIND_PRIMARY_PROVIDER:openai}\napp.sokomind.default-locale=${SOKOMIND_DEFAULT_LOCALE:en-KE}\napp.sokomind.openai.api-key=${OPENAI_API_KEY:}\napp.sokomind.openai.base-url=${OPENAI_BASE_URL:}\napp.sokomind.openai.mini-model=${SOKOMIND_OPENAI_MINI_MODEL:gpt-4o-mini}\napp.sokomind.openai.smart-model=${SOKOMIND_OPENAI_SMART_MODEL:gpt-4.1}\napp.sokomind.openai.vision-model=${SOKOMIND_OPENAI_VISION_MODEL:gpt-4o}\napp.sokomind.anthropic.api-key=${ANTHROPIC_API_KEY:}\napp.sokomind.anthropic.base-url=${ANTHROPIC_BASE_URL:}\napp.sokomind.anthropic.mini-model=${SOKOMIND_ANTHROPIC_MINI_MODEL:claude-haiku-4-5-20251001}\napp.sokomind.anthropic.smart-model=${SOKOMIND_ANTHROPIC_SMART_MODEL:claude-sonnet-4-5-20250929}\napp.sokomind.deepseek.api-key=${SOKOMIND_DEEPSEEK_API_KEY:${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}}\napp.sokomind.deepseek.base-url=${SOKOMIND_DEEPSEEK_BASE_URL:https://deepseek-v31.p.rapidapi.com/}\napp.sokomind.deepseek.host=${SOKOMIND_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.sokomind.deepseek.model=${SOKOMIND_DEEPSEEK_MODEL:DeepSeek-V3-0324}\napp.sokomind.industry-compare-enabled=${SOKOMIND_INDUSTRY_COMPARE_ENABLED:false}\napp.sokomind.industry-compare-min-twins=${SOKOMIND_INDUSTRY_COMPARE_MIN_TWINS:8}\napp.sokomind.max-tool-calls-per-request=${SOKOMIND_MAX_TOOL_CALLS:8}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# Meta webhook: Callback URL = ${API_PUBLIC_BASE_URL}/webhooks/whatsapp (same verify token in Meta dashboard).\napp.messaging.meta-whatsapp.webhook-verify-token=${WHATSAPP_META_WEBHOOK_VERIFY_TOKEN:}\napp.messaging.meta-whatsapp.app-secret=${WHATSAPP_META_APP_SECRET:}\n# SMS: none (log only) | africas_talking | sozuri | textsms\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\napp.messaging.sms.sozuri-project=${SOZURI_PROJECT:}\napp.messaging.sms.sozuri-api-key=${SOZURI_API_KEY:}\napp.messaging.sms.sozuri-from=${SOZURI_FROM:Sozuri}\napp.messaging.sms.sozuri-type=${SOZURI_TYPE:transactional}\napp.messaging.sms.sozuri-api-url=${SOZURI_API_URL:https://sozuri.net/api/v1/messaging}\n# When SMS is not configured, claim still works by returning the OTP in the JSON (local/dev).\n# Set false in production once Super Admin → Platform → Integrations has Sozuri/TextSMS.\napp.supplier-portal.claim.return-otp-when-stubbed=${APP_SUPPLIER_PORTAL_RETURN_OTP:true}\n# Sozuri callbacks: {API_PUBLIC_BASE_URL}/webhooks/sozuri/inbox and /webhooks/sozuri/delivery\napp.messaging.sms.textsms-partner-id=${TEXTSMS_PARTNER_ID:}\napp.messaging.sms.textsms-api-key=${TEXTSMS_API_KEY:}\napp.messaging.sms.textsms-shortcode=${TEXTSMS_SHORTCODE:}\napp.messaging.sms.textsms-api-url=${TEXTSMS_API_URL:https://sms.textsms.co.ke/api/services/sendsms/}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\n\n# Daily stock audit — random sample of products sold yesterday (branch-scoped manifest).\n# Per-business override: inventory.stocktake.dailyAuditSampleSize (Business settings → Stock take).\napp.inventory.daily-stock-audit.enabled=true\napp.inventory.daily-stock-audit.cron=0 0 6 * * *\napp.inventory.daily-stock-audit.zone=Africa/Nairobi\napp.inventory.daily-stock-audit.sample-size=25\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 4 — async global catalog adopt / promote jobs.\napp.global-catalog.jobs.worker.enabled=true\napp.global-catalog.jobs.worker.poll-interval-ms=3000\napp.global-catalog.jobs.worker.initial-delay-ms=5000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "e84f49a7df1b44321d87c421c774ba4df569362a": {"t": 1785752696.9833171, "n": 18569, "h": "696c3dcde6e6aa71", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\n# Comma-separated ISO country codes enabled for cloud self-serve onboarding.\n# A country must also have a published regional catalog (see RegionDefaults).\n# UG is enabled with cash/credit-only exception (see cash-credit-only-countries).\napp.selfserve.countries=${APP_SELFSERVE_COUNTRIES:KE,UG}\n# Countries without a local mobile-money rail yet (explicit product exception).\napp.selfserve.cash-credit-only-countries=${APP_SELFSERVE_CASH_CREDIT_ONLY_COUNTRIES:UG}\n\n# Native mobile distribution (deep links, platform store URLs, bundle IDs).\napp.mobile.api-base-url=${APP_MOBILE_API_BASE_URL:https://api.kiosk.ke}\napp.mobile.deep-link-scheme=${APP_MOBILE_DEEP_LINK_SCHEME:kiosk}\napp.mobile.universal-link-scheme=${APP_MOBILE_UNIVERSAL_LINK_SCHEME:https}\napp.mobile.store-links.ios=${APP_MOBILE_STORE_LINK_IOS:}\napp.mobile.store-links.android=${APP_MOBILE_STORE_LINK_ANDROID:}\napp.mobile.platform-bundles.shopper=${APP_MOBILE_BUNDLE_SHOPPER:com.kioskke.shopper}\napp.mobile.platform-bundles.cashier=${APP_MOBILE_BUNDLE_CASHIER:com.kioskke.cashier}\napp.mobile.platform-bundles.grocery=${APP_MOBILE_BUNDLE_GROCERY:com.kioskke.grocery}\napp.mobile.platform-bundles.admin=${APP_MOBILE_BUNDLE_ADMIN:com.kioskke.admin}\napp.mobile.platform-bundles.stock=${APP_MOBILE_BUNDLE_STOCK:com.kioskke.stock}\n# Optional: GitHub Actions dispatch when a tenant requests an App Store build (repository_dispatch).\napp.mobile.publish.github-token=${APP_MOBILE_PUBLISH_GITHUB_TOKEN:}\napp.mobile.publish.github-repo=${APP_MOBILE_PUBLISH_GITHUB_REPO:}\napp.mobile.publish.event-type=${APP_MOBILE_PUBLISH_EVENT_TYPE:mobile-tenant-publish}\n# Shared secret for GitHub Actions → POST /webhooks/mobile-publish/status (openssl rand -base64 32).\napp.mobile.publish.callback-secret=${APP_MOBILE_PUBLISH_CALLBACK_SECRET:}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=${MYSQL_PASSWORD:}\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n# Allow pending lower versions (e.g. V161) when higher versions already ran — common when branches land out of sequence.\nspring.flyway.out-of-order=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=${APP_JWT_SECRET:}\napp.jwt.access-ttl-minutes=60\n\n# HttpOnly refresh token cookie (JSON body omits refreshToken when enabled).\napp.auth.refresh-token-cookie-enabled=true\napp.auth.refresh-cookie-domain=${APP_AUTH_REFRESH_COOKIE_DOMAIN:}\napp.auth.refresh-cookie-secure=${APP_AUTH_REFRESH_COOKIE_SECURE:false}\napp.jwt.refresh-ttl-days=30\n# Revoke refresh when no activity for this many hours (sliding window, updated on refresh).\napp.auth.idle-timeout-hours=12\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.ke,www.kiosk.ke,kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\napp.security.refresh-rate-limit-per-minute=30\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Recurring balance reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Sends WhatsApp/SMS every interval-days to customers with an outstanding tab balance.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.interval-days=3\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.max-count=5\napp.credits.reminders.zone=Africa/Nairobi\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# Meta webhook: Callback URL = ${API_PUBLIC_BASE_URL}/webhooks/whatsapp (same verify token in Meta dashboard).\napp.messaging.meta-whatsapp.webhook-verify-token=${WHATSAPP_META_WEBHOOK_VERIFY_TOKEN:}\napp.messaging.meta-whatsapp.app-secret=${WHATSAPP_META_APP_SECRET:}\n# SMS: none (log only) | africas_talking | sozuri | textsms\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\napp.messaging.sms.sozuri-project=${SOZURI_PROJECT:}\napp.messaging.sms.sozuri-api-key=${SOZURI_API_KEY:}\napp.messaging.sms.sozuri-from=${SOZURI_FROM:Sozuri}\napp.messaging.sms.sozuri-type=${SOZURI_TYPE:transactional}\napp.messaging.sms.sozuri-api-url=${SOZURI_API_URL:https://sozuri.net/api/v1/messaging}\n# When SMS is not configured, claim still works by returning the OTP in the JSON (local/dev).\n# Set false in production once Super Admin → Platform → Integrations has Sozuri/TextSMS.\napp.supplier-portal.claim.return-otp-when-stubbed=${APP_SUPPLIER_PORTAL_RETURN_OTP:true}\n# Sozuri callbacks: {API_PUBLIC_BASE_URL}/webhooks/sozuri/inbox and /webhooks/sozuri/delivery\napp.messaging.sms.textsms-partner-id=${TEXTSMS_PARTNER_ID:}\napp.messaging.sms.textsms-api-key=${TEXTSMS_API_KEY:}\napp.messaging.sms.textsms-shortcode=${TEXTSMS_SHORTCODE:}\napp.messaging.sms.textsms-api-url=${TEXTSMS_API_URL:https://sms.textsms.co.ke/api/services/sendsms/}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\n\n# Daily stock audit — random sample of products sold yesterday (branch-scoped manifest).\n# Per-business override: inventory.stocktake.dailyAuditSampleSize (Business settings → Stock take).\napp.inventory.daily-stock-audit.enabled=true\napp.inventory.daily-stock-audit.cron=0 0 6 * * *\napp.inventory.daily-stock-audit.zone=Africa/Nairobi\napp.inventory.daily-stock-audit.sample-size=25\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 4 — async global catalog adopt / promote jobs.\napp.global-catalog.jobs.worker.enabled=true\napp.global-catalog.jobs.worker.poll-interval-ms=3000\napp.global-catalog.jobs.worker.initial-delay-ms=5000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "93123d2320b21b152a6609924347f8b1f7e2d0d0": {"t": 1785752700.187441, "n": 18297, "h": "f9122b96fb21a81a", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\n# Comma-separated ISO country codes enabled for cloud self-serve onboarding.\n# A country must also have a published regional catalog (see RegionDefaults).\n# UG is enabled with cash/credit-only exception (see cash-credit-only-countries).\napp.selfserve.countries=${APP_SELFSERVE_COUNTRIES:KE,UG}\n# Countries without a local mobile-money rail yet (explicit product exception).\napp.selfserve.cash-credit-only-countries=${APP_SELFSERVE_CASH_CREDIT_ONLY_COUNTRIES:UG}\n\n# Native mobile distribution (deep links, platform store URLs, bundle IDs).\napp.mobile.api-base-url=${APP_MOBILE_API_BASE_URL:https://api.kiosk.ke}\napp.mobile.deep-link-scheme=${APP_MOBILE_DEEP_LINK_SCHEME:kiosk}\napp.mobile.universal-link-scheme=${APP_MOBILE_UNIVERSAL_LINK_SCHEME:https}\napp.mobile.store-links.ios=${APP_MOBILE_STORE_LINK_IOS:}\napp.mobile.store-links.android=${APP_MOBILE_STORE_LINK_ANDROID:}\napp.mobile.platform-bundles.shopper=${APP_MOBILE_BUNDLE_SHOPPER:com.kioskke.shopper}\napp.mobile.platform-bundles.cashier=${APP_MOBILE_BUNDLE_CASHIER:com.kioskke.cashier}\napp.mobile.platform-bundles.grocery=${APP_MOBILE_BUNDLE_GROCERY:com.kioskke.grocery}\napp.mobile.platform-bundles.admin=${APP_MOBILE_BUNDLE_ADMIN:com.kioskke.admin}\napp.mobile.platform-bundles.stock=${APP_MOBILE_BUNDLE_STOCK:com.kioskke.stock}\n# Optional: GitHub Actions dispatch when a tenant requests an App Store build (repository_dispatch).\napp.mobile.publish.github-token=${APP_MOBILE_PUBLISH_GITHUB_TOKEN:}\napp.mobile.publish.github-repo=${APP_MOBILE_PUBLISH_GITHUB_REPO:}\napp.mobile.publish.event-type=${APP_MOBILE_PUBLISH_EVENT_TYPE:mobile-tenant-publish}\n# Shared secret for GitHub Actions → POST /webhooks/mobile-publish/status (openssl rand -base64 32).\napp.mobile.publish.callback-secret=${APP_MOBILE_PUBLISH_CALLBACK_SECRET:}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=${MYSQL_PASSWORD:}\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n# Allow pending lower versions (e.g. V161) when higher versions already ran — common when branches land out of sequence.\nspring.flyway.out-of-order=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=${APP_JWT_SECRET:}\napp.jwt.access-ttl-minutes=60\n\n# HttpOnly refresh token cookie (JSON body omits refreshToken when enabled).\napp.auth.refresh-token-cookie-enabled=true\napp.auth.refresh-cookie-domain=${APP_AUTH_REFRESH_COOKIE_DOMAIN:}\napp.auth.refresh-cookie-secure=${APP_AUTH_REFRESH_COOKIE_SECURE:false}\napp.jwt.refresh-ttl-days=30\n# Revoke refresh when no activity for this many hours (sliding window, updated on refresh).\napp.auth.idle-timeout-hours=12\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.ke,www.kiosk.ke,kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\napp.security.refresh-rate-limit-per-minute=30\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Recurring balance reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Sends WhatsApp/SMS every interval-days to customers with an outstanding tab balance.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.interval-days=3\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.max-count=5\napp.credits.reminders.zone=Africa/Nairobi\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# Meta webhook: Callback URL = ${API_PUBLIC_BASE_URL}/webhooks/whatsapp (same verify token in Meta dashboard).\napp.messaging.meta-whatsapp.webhook-verify-token=${WHATSAPP_META_WEBHOOK_VERIFY_TOKEN:}\napp.messaging.meta-whatsapp.app-secret=${WHATSAPP_META_APP_SECRET:}\n# SMS: none (log only) | africas_talking | sozuri | textsms\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\napp.messaging.sms.sozuri-project=${SOZURI_PROJECT:}\napp.messaging.sms.sozuri-api-key=${SOZURI_API_KEY:}\napp.messaging.sms.sozuri-from=${SOZURI_FROM:Sozuri}\napp.messaging.sms.sozuri-type=${SOZURI_TYPE:transactional}\napp.messaging.sms.sozuri-api-url=${SOZURI_API_URL:https://sozuri.net/api/v1/messaging}\n# Sozuri callbacks: {API_PUBLIC_BASE_URL}/webhooks/sozuri/inbox and /webhooks/sozuri/delivery\napp.messaging.sms.textsms-partner-id=${TEXTSMS_PARTNER_ID:}\napp.messaging.sms.textsms-api-key=${TEXTSMS_API_KEY:}\napp.messaging.sms.textsms-shortcode=${TEXTSMS_SHORTCODE:}\napp.messaging.sms.textsms-api-url=${TEXTSMS_API_URL:https://sms.textsms.co.ke/api/services/sendsms/}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\n\n# Daily stock audit — random sample of products sold yesterday (branch-scoped manifest).\n# Per-business override: inventory.stocktake.dailyAuditSampleSize (Business settings → Stock take).\napp.inventory.daily-stock-audit.enabled=true\napp.inventory.daily-stock-audit.cron=0 0 6 * * *\napp.inventory.daily-stock-audit.zone=Africa/Nairobi\napp.inventory.daily-stock-audit.sample-size=25\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 4 — async global catalog adopt / promote jobs.\napp.global-catalog.jobs.worker.enabled=true\napp.global-catalog.jobs.worker.poll-interval-ms=3000\napp.global-catalog.jobs.worker.initial-delay-ms=5000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "1b9858c98496d3e975ca0fe4d233b95127de819d": {"t": 1785752701.9957364, "n": 18275, "h": "f702743599aca3cb", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\n# Comma-separated ISO country codes enabled for cloud self-serve onboarding.\n# A country must also have a published regional catalog (see RegionDefaults).\n# UG is enabled with cash/credit-only exception (see cash-credit-only-countries).\napp.selfserve.countries=${APP_SELFSERVE_COUNTRIES:KE,UG}\n# Countries without a local mobile-money rail yet (explicit product exception).\napp.selfserve.cash-credit-only-countries=${APP_SELFSERVE_CASH_CREDIT_ONLY_COUNTRIES:UG}\n\n# Native mobile distribution (deep links, platform store URLs, bundle IDs).\napp.mobile.api-base-url=${APP_MOBILE_API_BASE_URL:https://api.kiosk.ke}\napp.mobile.deep-link-scheme=${APP_MOBILE_DEEP_LINK_SCHEME:kiosk}\napp.mobile.universal-link-scheme=${APP_MOBILE_UNIVERSAL_LINK_SCHEME:https}\napp.mobile.store-links.ios=${APP_MOBILE_STORE_LINK_IOS:}\napp.mobile.store-links.android=${APP_MOBILE_STORE_LINK_ANDROID:}\napp.mobile.platform-bundles.shopper=${APP_MOBILE_BUNDLE_SHOPPER:com.kioskke.shopper}\napp.mobile.platform-bundles.cashier=${APP_MOBILE_BUNDLE_CASHIER:com.kioskke.cashier}\napp.mobile.platform-bundles.grocery=${APP_MOBILE_BUNDLE_GROCERY:com.kioskke.grocery}\napp.mobile.platform-bundles.admin=${APP_MOBILE_BUNDLE_ADMIN:com.kioskke.admin}\napp.mobile.platform-bundles.stock=${APP_MOBILE_BUNDLE_STOCK:com.kioskke.stock}\n# Optional: GitHub Actions dispatch when a tenant requests an App Store build (repository_dispatch).\napp.mobile.publish.github-token=${APP_MOBILE_PUBLISH_GITHUB_TOKEN:}\napp.mobile.publish.github-repo=${APP_MOBILE_PUBLISH_GITHUB_REPO:}\napp.mobile.publish.event-type=${APP_MOBILE_PUBLISH_EVENT_TYPE:mobile-tenant-publish}\n# Shared secret for GitHub Actions → POST /webhooks/mobile-publish/status (openssl rand -base64 32).\napp.mobile.publish.callback-secret=${APP_MOBILE_PUBLISH_CALLBACK_SECRET:}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=${MYSQL_PASSWORD:}\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n# Allow pending lower versions (e.g. V161) when higher versions already ran — common when branches land out of sequence.\nspring.flyway.out-of-order=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=${APP_JWT_SECRET:}\napp.jwt.access-ttl-minutes=60\n\n# HttpOnly refresh token cookie (JSON body omits refreshToken when enabled).\napp.auth.refresh-token-cookie-enabled=true\napp.auth.refresh-cookie-domain=${APP_AUTH_REFRESH_COOKIE_DOMAIN:}\napp.auth.refresh-cookie-secure=${APP_AUTH_REFRESH_COOKIE_SECURE:false}\napp.jwt.refresh-ttl-days=30\n# Revoke refresh when no activity for this many hours (sliding window, updated on refresh).\napp.auth.idle-timeout-hours=12\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\napp.security.refresh-rate-limit-per-minute=30\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Recurring balance reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Sends WhatsApp/SMS every interval-days to customers with an outstanding tab balance.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.interval-days=3\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.max-count=5\napp.credits.reminders.zone=Africa/Nairobi\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# Meta webhook: Callback URL = ${API_PUBLIC_BASE_URL}/webhooks/whatsapp (same verify token in Meta dashboard).\napp.messaging.meta-whatsapp.webhook-verify-token=${WHATSAPP_META_WEBHOOK_VERIFY_TOKEN:}\napp.messaging.meta-whatsapp.app-secret=${WHATSAPP_META_APP_SECRET:}\n# SMS: none (log only) | africas_talking | sozuri | textsms\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\napp.messaging.sms.sozuri-project=${SOZURI_PROJECT:}\napp.messaging.sms.sozuri-api-key=${SOZURI_API_KEY:}\napp.messaging.sms.sozuri-from=${SOZURI_FROM:Sozuri}\napp.messaging.sms.sozuri-type=${SOZURI_TYPE:transactional}\napp.messaging.sms.sozuri-api-url=${SOZURI_API_URL:https://sozuri.net/api/v1/messaging}\n# Sozuri callbacks: {API_PUBLIC_BASE_URL}/webhooks/sozuri/inbox and /webhooks/sozuri/delivery\napp.messaging.sms.textsms-partner-id=${TEXTSMS_PARTNER_ID:}\napp.messaging.sms.textsms-api-key=${TEXTSMS_API_KEY:}\napp.messaging.sms.textsms-shortcode=${TEXTSMS_SHORTCODE:}\napp.messaging.sms.textsms-api-url=${TEXTSMS_API_URL:https://sms.textsms.co.ke/api/services/sendsms/}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\n\n# Daily stock audit — random sample of products sold yesterday (branch-scoped manifest).\n# Per-business override: inventory.stocktake.dailyAuditSampleSize (Business settings → Stock take).\napp.inventory.daily-stock-audit.enabled=true\napp.inventory.daily-stock-audit.cron=0 0 6 * * *\napp.inventory.daily-stock-audit.zone=Africa/Nairobi\napp.inventory.daily-stock-audit.sample-size=25\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 4 — async global catalog adopt / promote jobs.\napp.global-catalog.jobs.worker.enabled=true\napp.global-catalog.jobs.worker.poll-interval-ms=3000\napp.global-catalog.jobs.worker.initial-delay-ms=5000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "9a119b0fe6b3e0d3b0b4300dacfb6ee42ff45114": {"t": 1785752703.8954961, "n": 18122, "h": "8823983274e3fc0e", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\n# Comma-separated ISO country codes enabled for cloud self-serve onboarding.\n# A country must also have a published regional catalog (see RegionDefaults).\n# UG is enabled with cash/credit-only exception (see cash-credit-only-countries).\napp.selfserve.countries=${APP_SELFSERVE_COUNTRIES:KE,UG}\n# Countries without a local mobile-money rail yet (explicit product exception).\napp.selfserve.cash-credit-only-countries=${APP_SELFSERVE_CASH_CREDIT_ONLY_COUNTRIES:UG}\n\n# Native mobile distribution (deep links, platform store URLs, bundle IDs).\napp.mobile.api-base-url=${APP_MOBILE_API_BASE_URL:https://api.kiosk.ke}\napp.mobile.deep-link-scheme=${APP_MOBILE_DEEP_LINK_SCHEME:kiosk}\napp.mobile.universal-link-scheme=${APP_MOBILE_UNIVERSAL_LINK_SCHEME:https}\napp.mobile.store-links.ios=${APP_MOBILE_STORE_LINK_IOS:}\napp.mobile.store-links.android=${APP_MOBILE_STORE_LINK_ANDROID:}\napp.mobile.platform-bundles.shopper=${APP_MOBILE_BUNDLE_SHOPPER:com.kioskke.shopper}\napp.mobile.platform-bundles.cashier=${APP_MOBILE_BUNDLE_CASHIER:com.kioskke.cashier}\napp.mobile.platform-bundles.grocery=${APP_MOBILE_BUNDLE_GROCERY:com.kioskke.grocery}\napp.mobile.platform-bundles.admin=${APP_MOBILE_BUNDLE_ADMIN:com.kioskke.admin}\napp.mobile.platform-bundles.stock=${APP_MOBILE_BUNDLE_STOCK:com.kioskke.stock}\n# Optional: GitHub Actions dispatch when a tenant requests an App Store build (repository_dispatch).\napp.mobile.publish.github-token=${APP_MOBILE_PUBLISH_GITHUB_TOKEN:}\napp.mobile.publish.github-repo=${APP_MOBILE_PUBLISH_GITHUB_REPO:}\napp.mobile.publish.event-type=${APP_MOBILE_PUBLISH_EVENT_TYPE:mobile-tenant-publish}\n# Shared secret for GitHub Actions → POST /webhooks/mobile-publish/status (openssl rand -base64 32).\napp.mobile.publish.callback-secret=${APP_MOBILE_PUBLISH_CALLBACK_SECRET:}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=${MYSQL_PASSWORD:}\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=${APP_JWT_SECRET:}\napp.jwt.access-ttl-minutes=60\n\n# HttpOnly refresh token cookie (JSON body omits refreshToken when enabled).\napp.auth.refresh-token-cookie-enabled=true\napp.auth.refresh-cookie-domain=${APP_AUTH_REFRESH_COOKIE_DOMAIN:}\napp.auth.refresh-cookie-secure=${APP_AUTH_REFRESH_COOKIE_SECURE:false}\napp.jwt.refresh-ttl-days=30\n# Revoke refresh when no activity for this many hours (sliding window, updated on refresh).\napp.auth.idle-timeout-hours=12\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\napp.security.refresh-rate-limit-per-minute=30\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Recurring balance reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Sends WhatsApp/SMS every interval-days to customers with an outstanding tab balance.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.interval-days=3\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.max-count=5\napp.credits.reminders.zone=Africa/Nairobi\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# Meta webhook: Callback URL = ${API_PUBLIC_BASE_URL}/webhooks/whatsapp (same verify token in Meta dashboard).\napp.messaging.meta-whatsapp.webhook-verify-token=${WHATSAPP_META_WEBHOOK_VERIFY_TOKEN:}\napp.messaging.meta-whatsapp.app-secret=${WHATSAPP_META_APP_SECRET:}\n# SMS: none (log only) | africas_talking | sozuri | textsms\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\napp.messaging.sms.sozuri-project=${SOZURI_PROJECT:}\napp.messaging.sms.sozuri-api-key=${SOZURI_API_KEY:}\napp.messaging.sms.sozuri-from=${SOZURI_FROM:Sozuri}\napp.messaging.sms.sozuri-type=${SOZURI_TYPE:transactional}\napp.messaging.sms.sozuri-api-url=${SOZURI_API_URL:https://sozuri.net/api/v1/messaging}\n# Sozuri callbacks: {API_PUBLIC_BASE_URL}/webhooks/sozuri/inbox and /webhooks/sozuri/delivery\napp.messaging.sms.textsms-partner-id=${TEXTSMS_PARTNER_ID:}\napp.messaging.sms.textsms-api-key=${TEXTSMS_API_KEY:}\napp.messaging.sms.textsms-shortcode=${TEXTSMS_SHORTCODE:}\napp.messaging.sms.textsms-api-url=${TEXTSMS_API_URL:https://sms.textsms.co.ke/api/services/sendsms/}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\n\n# Daily stock audit — random sample of products sold yesterday (branch-scoped manifest).\n# Per-business override: inventory.stocktake.dailyAuditSampleSize (Business settings → Stock take).\napp.inventory.daily-stock-audit.enabled=true\napp.inventory.daily-stock-audit.cron=0 0 6 * * *\napp.inventory.daily-stock-audit.zone=Africa/Nairobi\napp.inventory.daily-stock-audit.sample-size=25\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 4 — async global catalog adopt / promote jobs.\napp.global-catalog.jobs.worker.enabled=true\napp.global-catalog.jobs.worker.poll-interval-ms=3000\napp.global-catalog.jobs.worker.initial-delay-ms=5000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "efe580adbd81145b7df686dc6b4af0738520f739": {"t": 1785752705.436139, "n": 17659, "h": "d3f640706a298e0a", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\n# Native mobile distribution (deep links, platform store URLs, bundle IDs).\napp.mobile.api-base-url=${APP_MOBILE_API_BASE_URL:https://api.kiosk.ke}\napp.mobile.deep-link-scheme=${APP_MOBILE_DEEP_LINK_SCHEME:kiosk}\napp.mobile.universal-link-scheme=${APP_MOBILE_UNIVERSAL_LINK_SCHEME:https}\napp.mobile.store-links.ios=${APP_MOBILE_STORE_LINK_IOS:}\napp.mobile.store-links.android=${APP_MOBILE_STORE_LINK_ANDROID:}\napp.mobile.platform-bundles.shopper=${APP_MOBILE_BUNDLE_SHOPPER:com.kioskke.shopper}\napp.mobile.platform-bundles.cashier=${APP_MOBILE_BUNDLE_CASHIER:com.kioskke.cashier}\napp.mobile.platform-bundles.grocery=${APP_MOBILE_BUNDLE_GROCERY:com.kioskke.grocery}\napp.mobile.platform-bundles.admin=${APP_MOBILE_BUNDLE_ADMIN:com.kioskke.admin}\napp.mobile.platform-bundles.stock=${APP_MOBILE_BUNDLE_STOCK:com.kioskke.stock}\n# Optional: GitHub Actions dispatch when a tenant requests an App Store build (repository_dispatch).\napp.mobile.publish.github-token=${APP_MOBILE_PUBLISH_GITHUB_TOKEN:}\napp.mobile.publish.github-repo=${APP_MOBILE_PUBLISH_GITHUB_REPO:}\napp.mobile.publish.event-type=${APP_MOBILE_PUBLISH_EVENT_TYPE:mobile-tenant-publish}\n# Shared secret for GitHub Actions → POST /webhooks/mobile-publish/status (openssl rand -base64 32).\napp.mobile.publish.callback-secret=${APP_MOBILE_PUBLISH_CALLBACK_SECRET:}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=${MYSQL_PASSWORD:}\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=${APP_JWT_SECRET:}\napp.jwt.access-ttl-minutes=60\n\n# HttpOnly refresh token cookie (JSON body omits refreshToken when enabled).\napp.auth.refresh-token-cookie-enabled=true\napp.auth.refresh-cookie-domain=${APP_AUTH_REFRESH_COOKIE_DOMAIN:}\napp.auth.refresh-cookie-secure=${APP_AUTH_REFRESH_COOKIE_SECURE:false}\napp.jwt.refresh-ttl-days=30\n# Revoke refresh when no activity for this many hours (sliding window, updated on refresh).\napp.auth.idle-timeout-hours=12\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\napp.security.refresh-rate-limit-per-minute=30\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Recurring balance reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Sends WhatsApp/SMS every interval-days to customers with an outstanding tab balance.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.interval-days=3\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.max-count=5\napp.credits.reminders.zone=Africa/Nairobi\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# Meta webhook: Callback URL = ${API_PUBLIC_BASE_URL}/webhooks/whatsapp (same verify token in Meta dashboard).\napp.messaging.meta-whatsapp.webhook-verify-token=${WHATSAPP_META_WEBHOOK_VERIFY_TOKEN:}\napp.messaging.meta-whatsapp.app-secret=${WHATSAPP_META_APP_SECRET:}\n# SMS: none (log only) | africas_talking | sozuri | textsms\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\napp.messaging.sms.sozuri-project=${SOZURI_PROJECT:}\napp.messaging.sms.sozuri-api-key=${SOZURI_API_KEY:}\napp.messaging.sms.sozuri-from=${SOZURI_FROM:Sozuri}\napp.messaging.sms.sozuri-type=${SOZURI_TYPE:transactional}\napp.messaging.sms.sozuri-api-url=${SOZURI_API_URL:https://sozuri.net/api/v1/messaging}\n# Sozuri callbacks: {API_PUBLIC_BASE_URL}/webhooks/sozuri/inbox and /webhooks/sozuri/delivery\napp.messaging.sms.textsms-partner-id=${TEXTSMS_PARTNER_ID:}\napp.messaging.sms.textsms-api-key=${TEXTSMS_API_KEY:}\napp.messaging.sms.textsms-shortcode=${TEXTSMS_SHORTCODE:}\napp.messaging.sms.textsms-api-url=${TEXTSMS_API_URL:https://sms.textsms.co.ke/api/services/sendsms/}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\n\n# Daily stock audit — random sample of products sold yesterday (branch-scoped manifest).\n# Per-business override: inventory.stocktake.dailyAuditSampleSize (Business settings → Stock take).\napp.inventory.daily-stock-audit.enabled=true\napp.inventory.daily-stock-audit.cron=0 0 6 * * *\napp.inventory.daily-stock-audit.zone=Africa/Nairobi\napp.inventory.daily-stock-audit.sample-size=25\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 4 — async global catalog adopt / promote jobs.\napp.global-catalog.jobs.worker.enabled=true\napp.global-catalog.jobs.worker.poll-interval-ms=3000\napp.global-catalog.jobs.worker.initial-delay-ms=5000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "85bdb8f18b3baa949461296a7072957254386e98": {"t": 1785752707.4714158, "n": 17453, "h": "1c059c550ad023a4", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\n# Native mobile distribution (deep links, platform store URLs, bundle IDs).\napp.mobile.api-base-url=${APP_MOBILE_API_BASE_URL:https://api.kiosk.ke}\napp.mobile.deep-link-scheme=${APP_MOBILE_DEEP_LINK_SCHEME:kiosk}\napp.mobile.universal-link-scheme=${APP_MOBILE_UNIVERSAL_LINK_SCHEME:https}\napp.mobile.store-links.ios=${APP_MOBILE_STORE_LINK_IOS:}\napp.mobile.store-links.android=${APP_MOBILE_STORE_LINK_ANDROID:}\napp.mobile.platform-bundles.shopper=${APP_MOBILE_BUNDLE_SHOPPER:com.kioskke.shopper}\napp.mobile.platform-bundles.cashier=${APP_MOBILE_BUNDLE_CASHIER:com.kioskke.cashier}\napp.mobile.platform-bundles.grocery=${APP_MOBILE_BUNDLE_GROCERY:com.kioskke.grocery}\napp.mobile.platform-bundles.admin=${APP_MOBILE_BUNDLE_ADMIN:com.kioskke.admin}\napp.mobile.platform-bundles.stock=${APP_MOBILE_BUNDLE_STOCK:com.kioskke.stock}\n# Optional: GitHub Actions dispatch when a tenant requests an App Store build (repository_dispatch).\napp.mobile.publish.github-token=${APP_MOBILE_PUBLISH_GITHUB_TOKEN:}\napp.mobile.publish.github-repo=${APP_MOBILE_PUBLISH_GITHUB_REPO:}\napp.mobile.publish.event-type=${APP_MOBILE_PUBLISH_EVENT_TYPE:mobile-tenant-publish}\n# Shared secret for GitHub Actions → POST /webhooks/mobile-publish/status (openssl rand -base64 32).\napp.mobile.publish.callback-secret=${APP_MOBILE_PUBLISH_CALLBACK_SECRET:}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=${MYSQL_PASSWORD:}\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=${APP_JWT_SECRET:}\napp.jwt.access-ttl-minutes=60\n\n# HttpOnly refresh token cookie (JSON body omits refreshToken when enabled).\napp.auth.refresh-token-cookie-enabled=true\napp.auth.refresh-cookie-domain=${APP_AUTH_REFRESH_COOKIE_DOMAIN:}\napp.auth.refresh-cookie-secure=${APP_AUTH_REFRESH_COOKIE_SECURE:false}\napp.jwt.refresh-ttl-days=30\n# Revoke refresh when no activity for this many hours (sliding window, updated on refresh).\napp.auth.idle-timeout-hours=12\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\napp.security.refresh-rate-limit-per-minute=30\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Recurring balance reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Sends WhatsApp/SMS every interval-days to customers with an outstanding tab balance.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.interval-days=3\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.max-count=5\napp.credits.reminders.zone=Africa/Nairobi\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# Meta webhook: Callback URL = ${API_PUBLIC_BASE_URL}/webhooks/whatsapp (same verify token in Meta dashboard).\napp.messaging.meta-whatsapp.webhook-verify-token=${WHATSAPP_META_WEBHOOK_VERIFY_TOKEN:}\napp.messaging.meta-whatsapp.app-secret=${WHATSAPP_META_APP_SECRET:}\n# SMS: none (log only) | africas_talking | sozuri | textsms\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\napp.messaging.sms.sozuri-project=${SOZURI_PROJECT:}\napp.messaging.sms.sozuri-api-key=${SOZURI_API_KEY:}\napp.messaging.sms.sozuri-from=${SOZURI_FROM:Sozuri}\napp.messaging.sms.sozuri-type=${SOZURI_TYPE:transactional}\napp.messaging.sms.sozuri-api-url=${SOZURI_API_URL:https://sozuri.net/api/v1/messaging}\n# Sozuri callbacks: {API_PUBLIC_BASE_URL}/webhooks/sozuri/inbox and /webhooks/sozuri/delivery\napp.messaging.sms.textsms-partner-id=${TEXTSMS_PARTNER_ID:}\napp.messaging.sms.textsms-api-key=${TEXTSMS_API_KEY:}\napp.messaging.sms.textsms-shortcode=${TEXTSMS_SHORTCODE:}\napp.messaging.sms.textsms-api-url=${TEXTSMS_API_URL:https://sms.textsms.co.ke/api/services/sendsms/}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\n\n# Daily stock audit — random sample of products sold yesterday (branch-scoped manifest).\n# Per-business override: inventory.stocktake.dailyAuditSampleSize (Business settings → Stock take).\napp.inventory.daily-stock-audit.enabled=true\napp.inventory.daily-stock-audit.cron=0 0 6 * * *\napp.inventory.daily-stock-audit.zone=Africa/Nairobi\napp.inventory.daily-stock-audit.sample-size=25\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "c72c6a612bd6451540133c093705d645a290222c": {"t": 1785752708.9553187, "n": 17466, "h": "2dd630083b5af549", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\n# Native mobile distribution (deep links, platform store URLs, bundle IDs).\napp.mobile.api-base-url=${APP_MOBILE_API_BASE_URL:https://api.kiosk.ke}\napp.mobile.deep-link-scheme=${APP_MOBILE_DEEP_LINK_SCHEME:kiosk}\napp.mobile.universal-link-scheme=${APP_MOBILE_UNIVERSAL_LINK_SCHEME:https}\napp.mobile.store-links.ios=${APP_MOBILE_STORE_LINK_IOS:}\napp.mobile.store-links.android=${APP_MOBILE_STORE_LINK_ANDROID:}\napp.mobile.platform-bundles.shopper=${APP_MOBILE_BUNDLE_SHOPPER:com.kioskke.shopper}\napp.mobile.platform-bundles.cashier=${APP_MOBILE_BUNDLE_CASHIER:com.kioskke.cashier}\napp.mobile.platform-bundles.grocery=${APP_MOBILE_BUNDLE_GROCERY:com.kioskke.grocery}\napp.mobile.platform-bundles.admin=${APP_MOBILE_BUNDLE_ADMIN:com.kioskke.admin}\napp.mobile.platform-bundles.stock=${APP_MOBILE_BUNDLE_STOCK:com.kioskke.stock}\n# Optional: GitHub Actions dispatch when a tenant requests an App Store build (repository_dispatch).\napp.mobile.publish.github-token=${APP_MOBILE_PUBLISH_GITHUB_TOKEN:}\napp.mobile.publish.github-repo=${APP_MOBILE_PUBLISH_GITHUB_REPO:}\napp.mobile.publish.event-type=${APP_MOBILE_PUBLISH_EVENT_TYPE:mobile-tenant-publish}\n# Shared secret for GitHub Actions → POST /webhooks/mobile-publish/status (openssl rand -base64 32).\napp.mobile.publish.callback-secret=${APP_MOBILE_PUBLISH_CALLBACK_SECRET:}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=${MYSQL_PASSWORD:}\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=${APP_JWT_SECRET:}\napp.jwt.access-ttl-minutes=60\n\n# HttpOnly refresh token cookie (JSON body omits refreshToken when enabled).\napp.auth.refresh-token-cookie-enabled=true\napp.auth.refresh-cookie-domain=${APP_AUTH_REFRESH_COOKIE_DOMAIN:}\napp.auth.refresh-cookie-secure=${APP_AUTH_REFRESH_COOKIE_SECURE:false}\napp.jwt.refresh-ttl-days=30\n# Revoke refresh when no activity for this many hours (sliding window, updated on refresh).\napp.auth.idle-timeout-hours=12\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\napp.security.refresh-rate-limit-per-minute=30\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Recurring balance reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Sends WhatsApp/SMS every interval-days to customers with an outstanding tab balance.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.interval-days=3\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.max-count=5\napp.credits.reminders.zone=Africa/Nairobi\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke/shop/account}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# Meta webhook: Callback URL = ${API_PUBLIC_BASE_URL}/webhooks/whatsapp (same verify token in Meta dashboard).\napp.messaging.meta-whatsapp.webhook-verify-token=${WHATSAPP_META_WEBHOOK_VERIFY_TOKEN:}\napp.messaging.meta-whatsapp.app-secret=${WHATSAPP_META_APP_SECRET:}\n# SMS: none (log only) | africas_talking | sozuri | textsms\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\napp.messaging.sms.sozuri-project=${SOZURI_PROJECT:}\napp.messaging.sms.sozuri-api-key=${SOZURI_API_KEY:}\napp.messaging.sms.sozuri-from=${SOZURI_FROM:Sozuri}\napp.messaging.sms.sozuri-type=${SOZURI_TYPE:transactional}\napp.messaging.sms.sozuri-api-url=${SOZURI_API_URL:https://sozuri.net/api/v1/messaging}\n# Sozuri callbacks: {API_PUBLIC_BASE_URL}/webhooks/sozuri/inbox and /webhooks/sozuri/delivery\napp.messaging.sms.textsms-partner-id=${TEXTSMS_PARTNER_ID:}\napp.messaging.sms.textsms-api-key=${TEXTSMS_API_KEY:}\napp.messaging.sms.textsms-shortcode=${TEXTSMS_SHORTCODE:}\napp.messaging.sms.textsms-api-url=${TEXTSMS_API_URL:https://sms.textsms.co.ke/api/services/sendsms/}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\n\n# Daily stock audit — random sample of products sold yesterday (branch-scoped manifest).\n# Per-business override: inventory.stocktake.dailyAuditSampleSize (Business settings → Stock take).\napp.inventory.daily-stock-audit.enabled=true\napp.inventory.daily-stock-audit.cron=0 0 6 * * *\napp.inventory.daily-stock-audit.zone=Africa/Nairobi\napp.inventory.daily-stock-audit.sample-size=25\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "e89e3b6558d678fa4359997e6614a454a50b8eb8": {"t": 1785752710.8837137, "n": 17183, "h": "bea2d26a417f7222", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\n# Native mobile distribution (deep links, platform store URLs, bundle IDs).\napp.mobile.api-base-url=${APP_MOBILE_API_BASE_URL:https://api.kiosk.ke}\napp.mobile.deep-link-scheme=${APP_MOBILE_DEEP_LINK_SCHEME:kiosk}\napp.mobile.universal-link-scheme=${APP_MOBILE_UNIVERSAL_LINK_SCHEME:https}\napp.mobile.store-links.ios=${APP_MOBILE_STORE_LINK_IOS:}\napp.mobile.store-links.android=${APP_MOBILE_STORE_LINK_ANDROID:}\napp.mobile.platform-bundles.shopper=${APP_MOBILE_BUNDLE_SHOPPER:com.kioskke.shopper}\napp.mobile.platform-bundles.cashier=${APP_MOBILE_BUNDLE_CASHIER:com.kioskke.cashier}\napp.mobile.platform-bundles.grocery=${APP_MOBILE_BUNDLE_GROCERY:com.kioskke.grocery}\napp.mobile.platform-bundles.admin=${APP_MOBILE_BUNDLE_ADMIN:com.kioskke.admin}\napp.mobile.platform-bundles.stock=${APP_MOBILE_BUNDLE_STOCK:com.kioskke.stock}\n# Optional: GitHub Actions dispatch when a tenant requests an App Store build (repository_dispatch).\napp.mobile.publish.github-token=${APP_MOBILE_PUBLISH_GITHUB_TOKEN:}\napp.mobile.publish.github-repo=${APP_MOBILE_PUBLISH_GITHUB_REPO:}\napp.mobile.publish.event-type=${APP_MOBILE_PUBLISH_EVENT_TYPE:mobile-tenant-publish}\n# Shared secret for GitHub Actions → POST /webhooks/mobile-publish/status (openssl rand -base64 32).\napp.mobile.publish.callback-secret=${APP_MOBILE_PUBLISH_CALLBACK_SECRET:}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=${MYSQL_PASSWORD:}\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=${APP_JWT_SECRET:}\napp.jwt.access-ttl-minutes=60\n\n# HttpOnly refresh token cookie (JSON body omits refreshToken when enabled).\napp.auth.refresh-token-cookie-enabled=true\napp.auth.refresh-cookie-domain=${APP_AUTH_REFRESH_COOKIE_DOMAIN:}\napp.auth.refresh-cookie-secure=${APP_AUTH_REFRESH_COOKIE_SECURE:false}\napp.jwt.refresh-ttl-days=30\n# Revoke refresh when no activity for this many hours (sliding window, updated on refresh).\napp.auth.idle-timeout-hours=12\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\napp.security.refresh-rate-limit-per-minute=30\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Recurring balance reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Sends WhatsApp/SMS every interval-days to customers with an outstanding tab balance.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.interval-days=3\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.max-count=5\napp.credits.reminders.zone=Africa/Nairobi\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke/shop/account}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# Meta webhook: Callback URL = ${API_PUBLIC_BASE_URL}/webhooks/whatsapp (same verify token in Meta dashboard).\napp.messaging.meta-whatsapp.webhook-verify-token=${WHATSAPP_META_WEBHOOK_VERIFY_TOKEN:}\napp.messaging.meta-whatsapp.app-secret=${WHATSAPP_META_APP_SECRET:}\n# SMS: none (log only) | africas_talking | sozuri\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\napp.messaging.sms.sozuri-project=${SOZURI_PROJECT:}\napp.messaging.sms.sozuri-api-key=${SOZURI_API_KEY:}\napp.messaging.sms.sozuri-from=${SOZURI_FROM:Sozuri}\napp.messaging.sms.sozuri-type=${SOZURI_TYPE:transactional}\napp.messaging.sms.sozuri-api-url=${SOZURI_API_URL:https://sozuri.net/api/v1/messaging}\n# Sozuri callbacks: {API_PUBLIC_BASE_URL}/webhooks/sozuri/inbox and /webhooks/sozuri/delivery\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\n\n# Daily stock audit — random sample of products sold yesterday (branch-scoped manifest).\n# Per-business override: inventory.stocktake.dailyAuditSampleSize (Business settings → Stock take).\napp.inventory.daily-stock-audit.enabled=true\napp.inventory.daily-stock-audit.cron=0 0 6 * * *\napp.inventory.daily-stock-audit.zone=Africa/Nairobi\napp.inventory.daily-stock-audit.sample-size=25\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "5d727f3316783b98e8053fccaaf7f5a7d47deeed": {"t": 1785752712.2861748, "n": 16778, "h": "100141e0b86b74b8", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\n# Native mobile distribution (deep links, platform store URLs, bundle IDs).\napp.mobile.api-base-url=${APP_MOBILE_API_BASE_URL:https://api.kiosk.ke}\napp.mobile.deep-link-scheme=${APP_MOBILE_DEEP_LINK_SCHEME:kiosk}\napp.mobile.universal-link-scheme=${APP_MOBILE_UNIVERSAL_LINK_SCHEME:https}\napp.mobile.store-links.ios=${APP_MOBILE_STORE_LINK_IOS:}\napp.mobile.store-links.android=${APP_MOBILE_STORE_LINK_ANDROID:}\napp.mobile.platform-bundles.shopper=${APP_MOBILE_BUNDLE_SHOPPER:com.kioskke.shopper}\napp.mobile.platform-bundles.cashier=${APP_MOBILE_BUNDLE_CASHIER:com.kioskke.cashier}\napp.mobile.platform-bundles.grocery=${APP_MOBILE_BUNDLE_GROCERY:com.kioskke.grocery}\napp.mobile.platform-bundles.admin=${APP_MOBILE_BUNDLE_ADMIN:com.kioskke.admin}\napp.mobile.platform-bundles.stock=${APP_MOBILE_BUNDLE_STOCK:com.kioskke.stock}\n# Optional: GitHub Actions dispatch when a tenant requests an App Store build (repository_dispatch).\napp.mobile.publish.github-token=${APP_MOBILE_PUBLISH_GITHUB_TOKEN:}\napp.mobile.publish.github-repo=${APP_MOBILE_PUBLISH_GITHUB_REPO:}\napp.mobile.publish.event-type=${APP_MOBILE_PUBLISH_EVENT_TYPE:mobile-tenant-publish}\n# Shared secret for GitHub Actions → POST /webhooks/mobile-publish/status (openssl rand -base64 32).\napp.mobile.publish.callback-secret=${APP_MOBILE_PUBLISH_CALLBACK_SECRET:}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=${MYSQL_PASSWORD:}\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=${APP_JWT_SECRET:}\napp.jwt.access-ttl-minutes=60\n\n# HttpOnly refresh token cookie (JSON body omits refreshToken when enabled).\napp.auth.refresh-token-cookie-enabled=true\napp.auth.refresh-cookie-domain=${APP_AUTH_REFRESH_COOKIE_DOMAIN:}\napp.auth.refresh-cookie-secure=${APP_AUTH_REFRESH_COOKIE_SECURE:false}\napp.jwt.refresh-ttl-days=30\n# Revoke refresh when no activity for this many hours (sliding window, updated on refresh).\napp.auth.idle-timeout-hours=12\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\napp.security.refresh-rate-limit-per-minute=30\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Recurring balance reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Sends WhatsApp/SMS every interval-days to customers with an outstanding tab balance.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.interval-days=3\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.max-count=5\napp.credits.reminders.zone=Africa/Nairobi\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke/shop/account}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# Meta webhook: Callback URL = ${API_PUBLIC_BASE_URL}/webhooks/whatsapp (same verify token in Meta dashboard).\napp.messaging.meta-whatsapp.webhook-verify-token=${WHATSAPP_META_WEBHOOK_VERIFY_TOKEN:}\napp.messaging.meta-whatsapp.app-secret=${WHATSAPP_META_APP_SECRET:}\n# SMS: none (log only) | africas_talking\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\n\n# Daily stock audit — random sample of products sold yesterday (branch-scoped manifest).\n# Per-business override: inventory.stocktake.dailyAuditSampleSize (Business settings → Stock take).\napp.inventory.daily-stock-audit.enabled=true\napp.inventory.daily-stock-audit.cron=0 0 6 * * *\napp.inventory.daily-stock-audit.zone=Africa/Nairobi\napp.inventory.daily-stock-audit.sample-size=25\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "fce136aba94bfc74d0c51a491beec80bfe91e2a1": {"t": 1785752713.6425211, "n": 16671, "h": "721dcac568aef707", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\n# Native mobile distribution (deep links, platform store URLs, bundle IDs).\napp.mobile.api-base-url=${APP_MOBILE_API_BASE_URL:https://api.kiosk.ke}\napp.mobile.deep-link-scheme=${APP_MOBILE_DEEP_LINK_SCHEME:kiosk}\napp.mobile.universal-link-scheme=${APP_MOBILE_UNIVERSAL_LINK_SCHEME:https}\napp.mobile.store-links.ios=${APP_MOBILE_STORE_LINK_IOS:}\napp.mobile.store-links.android=${APP_MOBILE_STORE_LINK_ANDROID:}\napp.mobile.platform-bundles.shopper=${APP_MOBILE_BUNDLE_SHOPPER:com.kioskke.shopper}\napp.mobile.platform-bundles.cashier=${APP_MOBILE_BUNDLE_CASHIER:com.kioskke.cashier}\napp.mobile.platform-bundles.grocery=${APP_MOBILE_BUNDLE_GROCERY:com.kioskke.grocery}\napp.mobile.platform-bundles.admin=${APP_MOBILE_BUNDLE_ADMIN:com.kioskke.admin}\napp.mobile.platform-bundles.stock=${APP_MOBILE_BUNDLE_STOCK:com.kioskke.stock}\n# Optional: GitHub Actions dispatch when a tenant requests an App Store build (repository_dispatch).\napp.mobile.publish.github-token=${APP_MOBILE_PUBLISH_GITHUB_TOKEN:}\napp.mobile.publish.github-repo=${APP_MOBILE_PUBLISH_GITHUB_REPO:}\napp.mobile.publish.event-type=${APP_MOBILE_PUBLISH_EVENT_TYPE:mobile-tenant-publish}\n# Shared secret for GitHub Actions → POST /webhooks/mobile-publish/status (openssl rand -base64 32).\napp.mobile.publish.callback-secret=${APP_MOBILE_PUBLISH_CALLBACK_SECRET:}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=${MYSQL_PASSWORD:}\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=${APP_JWT_SECRET:}\napp.jwt.access-ttl-minutes=60\n\n# HttpOnly refresh token cookie (JSON body omits refreshToken when enabled).\napp.auth.refresh-token-cookie-enabled=true\napp.auth.refresh-cookie-domain=${APP_AUTH_REFRESH_COOKIE_DOMAIN:}\napp.auth.refresh-cookie-secure=${APP_AUTH_REFRESH_COOKIE_SECURE:false}\napp.jwt.refresh-ttl-days=30\n# Revoke refresh when no activity for this many hours (sliding window, updated on refresh).\napp.auth.idle-timeout-hours=12\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\napp.security.refresh-rate-limit-per-minute=30\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Recurring balance reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Sends WhatsApp/SMS every interval-days to customers with an outstanding tab balance.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.interval-days=3\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.max-count=5\napp.credits.reminders.zone=Africa/Nairobi\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke/shop/account}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# Meta webhook: Callback URL = ${API_PUBLIC_BASE_URL}/webhooks/whatsapp (same verify token in Meta dashboard).\napp.messaging.meta-whatsapp.webhook-verify-token=${WHATSAPP_META_WEBHOOK_VERIFY_TOKEN:}\napp.messaging.meta-whatsapp.app-secret=${WHATSAPP_META_APP_SECRET:}\n# SMS: none (log only) | africas_talking\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\n\n# Daily stock audit — random 25 products sold yesterday (branch-scoped manifest).\napp.inventory.daily-stock-audit.enabled=true\napp.inventory.daily-stock-audit.cron=0 0 6 * * *\napp.inventory.daily-stock-audit.zone=Africa/Nairobi\napp.inventory.daily-stock-audit.sample-size=25\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "6dea6a50f2be01bcb30841fafaebfa9aa0c36687": {"t": 1785752716.5165312, "n": 16395, "h": "a78632422297d334", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\n# Native mobile distribution (deep links, platform store URLs, bundle IDs).\napp.mobile.api-base-url=${APP_MOBILE_API_BASE_URL:https://api.kiosk.ke}\napp.mobile.deep-link-scheme=${APP_MOBILE_DEEP_LINK_SCHEME:kiosk}\napp.mobile.universal-link-scheme=${APP_MOBILE_UNIVERSAL_LINK_SCHEME:https}\napp.mobile.store-links.ios=${APP_MOBILE_STORE_LINK_IOS:}\napp.mobile.store-links.android=${APP_MOBILE_STORE_LINK_ANDROID:}\napp.mobile.platform-bundles.shopper=${APP_MOBILE_BUNDLE_SHOPPER:com.kioskke.shopper}\napp.mobile.platform-bundles.cashier=${APP_MOBILE_BUNDLE_CASHIER:com.kioskke.cashier}\napp.mobile.platform-bundles.grocery=${APP_MOBILE_BUNDLE_GROCERY:com.kioskke.grocery}\napp.mobile.platform-bundles.admin=${APP_MOBILE_BUNDLE_ADMIN:com.kioskke.admin}\napp.mobile.platform-bundles.stock=${APP_MOBILE_BUNDLE_STOCK:com.kioskke.stock}\n# Optional: GitHub Actions dispatch when a tenant requests an App Store build (repository_dispatch).\napp.mobile.publish.github-token=${APP_MOBILE_PUBLISH_GITHUB_TOKEN:}\napp.mobile.publish.github-repo=${APP_MOBILE_PUBLISH_GITHUB_REPO:}\napp.mobile.publish.event-type=${APP_MOBILE_PUBLISH_EVENT_TYPE:mobile-tenant-publish}\n# Shared secret for GitHub Actions → POST /webhooks/mobile-publish/status (openssl rand -base64 32).\napp.mobile.publish.callback-secret=${APP_MOBILE_PUBLISH_CALLBACK_SECRET:}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=${MYSQL_PASSWORD:}\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=${APP_JWT_SECRET:}\napp.jwt.access-ttl-minutes=60\n\n# HttpOnly refresh token cookie (JSON body omits refreshToken when enabled).\napp.auth.refresh-token-cookie-enabled=true\napp.auth.refresh-cookie-domain=${APP_AUTH_REFRESH_COOKIE_DOMAIN:}\napp.auth.refresh-cookie-secure=${APP_AUTH_REFRESH_COOKIE_SECURE:false}\napp.jwt.refresh-ttl-days=30\n# Revoke refresh when no activity for this many hours (sliding window, updated on refresh).\napp.auth.idle-timeout-hours=12\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\napp.security.refresh-rate-limit-per-minute=30\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Recurring balance reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Sends WhatsApp/SMS every interval-days to customers with an outstanding tab balance.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.interval-days=3\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.max-count=5\napp.credits.reminders.zone=Africa/Nairobi\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke/shop/account}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# Meta webhook: Callback URL = ${API_PUBLIC_BASE_URL}/webhooks/whatsapp (same verify token in Meta dashboard).\napp.messaging.meta-whatsapp.webhook-verify-token=${WHATSAPP_META_WEBHOOK_VERIFY_TOKEN:}\napp.messaging.meta-whatsapp.app-secret=${WHATSAPP_META_APP_SECRET:}\n# SMS: none (log only) | africas_talking\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "a26c3072215003bf2739b6bc920915f9ae5d2cd0": {"t": 1785752717.8713446, "n": 16272, "h": "1d20c40c41816116", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\n# Native mobile distribution (deep links, platform store URLs, bundle IDs).\napp.mobile.api-base-url=${APP_MOBILE_API_BASE_URL:https://api.kiosk.ke}\napp.mobile.deep-link-scheme=${APP_MOBILE_DEEP_LINK_SCHEME:kiosk}\napp.mobile.universal-link-scheme=${APP_MOBILE_UNIVERSAL_LINK_SCHEME:https}\napp.mobile.store-links.ios=${APP_MOBILE_STORE_LINK_IOS:}\napp.mobile.store-links.android=${APP_MOBILE_STORE_LINK_ANDROID:}\napp.mobile.platform-bundles.shopper=${APP_MOBILE_BUNDLE_SHOPPER:com.kioskke.shopper}\napp.mobile.platform-bundles.cashier=${APP_MOBILE_BUNDLE_CASHIER:com.kioskke.cashier}\napp.mobile.platform-bundles.grocery=${APP_MOBILE_BUNDLE_GROCERY:com.kioskke.grocery}\napp.mobile.platform-bundles.admin=${APP_MOBILE_BUNDLE_ADMIN:com.kioskke.admin}\napp.mobile.platform-bundles.stock=${APP_MOBILE_BUNDLE_STOCK:com.kioskke.stock}\n# Optional: GitHub Actions dispatch when a tenant requests an App Store build (repository_dispatch).\napp.mobile.publish.github-token=${APP_MOBILE_PUBLISH_GITHUB_TOKEN:}\napp.mobile.publish.github-repo=${APP_MOBILE_PUBLISH_GITHUB_REPO:}\napp.mobile.publish.event-type=${APP_MOBILE_PUBLISH_EVENT_TYPE:mobile-tenant-publish}\n# Shared secret for GitHub Actions → POST /webhooks/mobile-publish/status (openssl rand -base64 32).\napp.mobile.publish.callback-secret=${APP_MOBILE_PUBLISH_CALLBACK_SECRET:}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=${MYSQL_PASSWORD:}\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=${APP_JWT_SECRET:}\napp.jwt.access-ttl-minutes=30\n\n# HttpOnly refresh token cookie (JSON body omits refreshToken when enabled).\napp.auth.refresh-token-cookie-enabled=true\napp.auth.refresh-cookie-domain=${APP_AUTH_REFRESH_COOKIE_DOMAIN:}\napp.auth.refresh-cookie-secure=${APP_AUTH_REFRESH_COOKIE_SECURE:false}\napp.jwt.refresh-ttl-days=30\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\napp.security.refresh-rate-limit-per-minute=30\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Recurring balance reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Sends WhatsApp/SMS every interval-days to customers with an outstanding tab balance.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.interval-days=3\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.max-count=5\napp.credits.reminders.zone=Africa/Nairobi\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke/shop/account}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# Meta webhook: Callback URL = ${API_PUBLIC_BASE_URL}/webhooks/whatsapp (same verify token in Meta dashboard).\napp.messaging.meta-whatsapp.webhook-verify-token=${WHATSAPP_META_WEBHOOK_VERIFY_TOKEN:}\napp.messaging.meta-whatsapp.app-secret=${WHATSAPP_META_APP_SECRET:}\n# SMS: none (log only) | africas_talking\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "da41424bb65ce0089057b5d4b53ce8339bcff13c": {"t": 1785752722.0996897, "n": 16005, "h": "eb15db45b3c1f27e", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\n# Native mobile distribution (deep links, platform store URLs, bundle IDs).\napp.mobile.api-base-url=${APP_MOBILE_API_BASE_URL:https://api.kiosk.ke}\napp.mobile.deep-link-scheme=${APP_MOBILE_DEEP_LINK_SCHEME:kiosk}\napp.mobile.universal-link-scheme=${APP_MOBILE_UNIVERSAL_LINK_SCHEME:https}\napp.mobile.store-links.ios=${APP_MOBILE_STORE_LINK_IOS:}\napp.mobile.store-links.android=${APP_MOBILE_STORE_LINK_ANDROID:}\napp.mobile.platform-bundles.shopper=${APP_MOBILE_BUNDLE_SHOPPER:com.kioskke.shopper}\napp.mobile.platform-bundles.cashier=${APP_MOBILE_BUNDLE_CASHIER:com.kioskke.cashier}\napp.mobile.platform-bundles.grocery=${APP_MOBILE_BUNDLE_GROCERY:com.kioskke.grocery}\napp.mobile.platform-bundles.admin=${APP_MOBILE_BUNDLE_ADMIN:com.kioskke.admin}\napp.mobile.platform-bundles.stock=${APP_MOBILE_BUNDLE_STOCK:com.kioskke.stock}\n# Optional: GitHub Actions dispatch when a tenant requests an App Store build (repository_dispatch).\napp.mobile.publish.github-token=${APP_MOBILE_PUBLISH_GITHUB_TOKEN:}\napp.mobile.publish.github-repo=${APP_MOBILE_PUBLISH_GITHUB_REPO:}\napp.mobile.publish.event-type=${APP_MOBILE_PUBLISH_EVENT_TYPE:mobile-tenant-publish}\n# Shared secret for GitHub Actions → POST /webhooks/mobile-publish/status (openssl rand -base64 32).\napp.mobile.publish.callback-secret=${APP_MOBILE_PUBLISH_CALLBACK_SECRET:}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=${MYSQL_PASSWORD:}\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=${APP_JWT_SECRET:}\napp.jwt.access-ttl-minutes=30\n\n# HttpOnly refresh token cookie (JSON body omits refreshToken when enabled).\napp.auth.refresh-token-cookie-enabled=true\napp.auth.refresh-cookie-domain=${APP_AUTH_REFRESH_COOKIE_DOMAIN:}\napp.auth.refresh-cookie-secure=${APP_AUTH_REFRESH_COOKIE_SECURE:false}\napp.jwt.refresh-ttl-days=30\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\napp.security.refresh-rate-limit-per-minute=30\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Recurring balance reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Sends WhatsApp/SMS every interval-days to customers with an outstanding tab balance.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.interval-days=3\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.max-count=5\napp.credits.reminders.zone=Africa/Nairobi\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke/shop/account}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# SMS: none (log only) | africas_talking\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "1b8cbda2c15b9e8c79ca25cfd51cf2d3bcb2ca02": {"t": 1785752725.4992027, "n": 14686, "h": "ff213c2f545117b3", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=${MYSQL_PASSWORD:}\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=${APP_JWT_SECRET:}\napp.jwt.access-ttl-minutes=30\n\n# HttpOnly refresh token cookie (JSON body omits refreshToken when enabled).\napp.auth.refresh-token-cookie-enabled=true\napp.auth.refresh-cookie-domain=${APP_AUTH_REFRESH_COOKIE_DOMAIN:}\napp.auth.refresh-cookie-secure=${APP_AUTH_REFRESH_COOKIE_SECURE:false}\napp.jwt.refresh-ttl-days=30\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\napp.security.refresh-rate-limit-per-minute=30\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Recurring balance reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Sends WhatsApp/SMS every interval-days to customers with an outstanding tab balance.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.interval-days=3\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.max-count=5\napp.credits.reminders.zone=Africa/Nairobi\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke/shop/account}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# SMS: none (log only) | africas_talking\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "c89aafeef2f17b15d61059a5bea7465f129654a9": {"t": 1785752727.9574764, "n": 14719, "h": "06e11a8a31d9e816", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=00870087Fab.#\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev\napp.jwt.access-ttl-minutes=30\n\n# HttpOnly refresh token cookie (JSON body omits refreshToken when enabled).\napp.auth.refresh-token-cookie-enabled=true\napp.auth.refresh-cookie-domain=${APP_AUTH_REFRESH_COOKIE_DOMAIN:}\napp.auth.refresh-cookie-secure=${APP_AUTH_REFRESH_COOKIE_SECURE:false}\napp.jwt.refresh-ttl-days=30\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\napp.security.refresh-rate-limit-per-minute=30\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Overdue-tab reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Provider dispatch is a stub until Phase 7 (`OverdueDebtReminderService.dispatch`); audit + idempotency are live.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.overdue-days=30\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.zone=UTC\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke/shop/account}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# SMS: none (log only) | africas_talking\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "a4ed35fcb9f6bf3c1944eebd92864ff0ce63eb5e": {"t": 1785752730.230366, "n": 14461, "h": "b22ab3d540214b28", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=00870087Fab.#\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev\napp.jwt.access-ttl-minutes=60\napp.jwt.refresh-ttl-days=30\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\napp.security.refresh-rate-limit-per-minute=30\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Overdue-tab reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Provider dispatch is a stub until Phase 7 (`OverdueDebtReminderService.dispatch`); audit + idempotency are live.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.overdue-days=30\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.zone=UTC\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke/shop/account}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# SMS: none (log only) | africas_talking\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "9892a34f628366380052e830a5e55beba85a6594": {"t": 1785752733.2103014, "n": 14415, "h": "aecf201d7df95d4b", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=00870087Fab.#\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev\napp.jwt.access-ttl-minutes=60\napp.jwt.refresh-ttl-days=30\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Overdue-tab reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Provider dispatch is a stub until Phase 7 (`OverdueDebtReminderService.dispatch`); audit + idempotency are live.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.overdue-days=30\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.zone=UTC\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke/shop/account}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# SMS: none (log only) | africas_talking\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "86338f61545ec7847a5648aeb1865e5dcdceebcc": {"t": 1785752735.511248, "n": 14044, "h": "bb51d7697699a462", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=00870087Fab.#\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev\napp.jwt.access-ttl-minutes=60\napp.jwt.refresh-ttl-days=30\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Overdue-tab reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Provider dispatch is a stub until Phase 7 (`OverdueDebtReminderService.dispatch`); audit + idempotency are live.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.overdue-days=30\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.zone=UTC\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke/shop/account}\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# SMS: none (log only) | africas_talking\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "c55f55d200005367e40da951437d2f39a1f22fc0": {"t": 1785752736.7838955, "n": 12169, "h": "1a967344a8634e9d", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=00870087Fab.#\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev\napp.jwt.access-ttl-minutes=60\napp.jwt.refresh-ttl-days=30\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Overdue-tab reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Provider dispatch is a stub until Phase 7 (`OverdueDebtReminderService.dispatch`); audit + idempotency are live.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.overdue-days=30\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.zone=UTC\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke/shop/account}\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# SMS: none (log only) | africas_talking\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "8ebe2ca811c7e9f1521564d82fd38e923691d931": {"t": 1785752739.9175491, "n": 11125, "h": "13e45d578c45aa7c", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=00870087Fab.#\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev\napp.jwt.access-ttl-minutes=60\napp.jwt.refresh-ttl-days=30\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Overdue-tab reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Provider dispatch is a stub until Phase 7 (`OverdueDebtReminderService.dispatch`); audit + idempotency are live.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.overdue-days=30\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.zone=UTC\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "14b0cbeab92bc592e6b164ad68bc80503dbb5f0d": {"t": 1785752741.2753546, "n": 10923, "h": "5b7faef76e3a06c9", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=00870087Fab.#\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev\napp.jwt.access-ttl-minutes=60\napp.jwt.refresh-ttl-days=30\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Overdue-tab reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Provider dispatch is a stub until Phase 7 (`OverdueDebtReminderService.dispatch`); audit + idempotency are live.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.overdue-days=30\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.zone=UTC\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "302846180256d5f7435ce4a950862f7820800210": {"t": 1785752742.908594, "n": 10699, "h": "8f18650fe2b61f46", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=00870087Fab.#\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev\napp.jwt.access-ttl-minutes=60\napp.jwt.refresh-ttl-days=30\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Overdue-tab reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Provider dispatch is a stub until Phase 7 (`OverdueDebtReminderService.dispatch`); audit + idempotency are live.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.overdue-days=30\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.zone=UTC\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "d9f9347aca72a56a2e9776f991d1b0ac141756c8": {"t": 1785752744.199939, "n": 10498, "h": "5bd625951571da6b", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=00870087Fab.#\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev\napp.jwt.access-ttl-minutes=60\napp.jwt.refresh-ttl-days=30\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Overdue-tab reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Provider dispatch is a stub until Phase 7 (`OverdueDebtReminderService.dispatch`); audit + idempotency are live.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.overdue-days=30\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.zone=UTC\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "a48217549802dff175628dd949ddf9ded5573625": {"t": 1785752746.974578, "n": 10462, "h": "d08a67b5d0f9ad9b", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=00870087Fab.#\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev\napp.jwt.access-ttl-minutes=60\napp.jwt.refresh-ttl-days=30\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Overdue-tab reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Provider dispatch is a stub until Phase 7 (`OverdueDebtReminderService.dispatch`); audit + idempotency are live.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.overdue-days=30\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.zone=UTC\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "9d0b252b5fc4f2a888a02399eb985ab08faac845": {"t": 1785752748.8908613, "n": 10462, "h": "85bad96a870686e8", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=00870087Fab.#\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev\napp.jwt.access-ttl-minutes=15\napp.jwt.refresh-ttl-days=30\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Overdue-tab reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Provider dispatch is a stub until Phase 7 (`OverdueDebtReminderService.dispatch`); audit + idempotency are live.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.overdue-days=30\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.zone=UTC\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "4ae99309627dd15c53e2390163008da2844321d6": {"t": 1785752750.480337, "n": 9871, "h": "2ececfc80b22f4e0", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\nserver.port=5050\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=00870087Fab.#\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev-dev\napp.jwt.access-ttl-minutes=15\napp.jwt.refresh-ttl-days=30\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Overdue-tab reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Provider dispatch is a stub until Phase 7 (`OverdueDebtReminderService.dispatch`); audit + idempotency are live.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.overdue-days=30\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.zone=UTC\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "e95fb67da05a7a50e2e210d5ce6f42352845e5ff": {"t": 1786067370.6012504, "n": 21246, "h": "6c1ef7f8420b5659", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\n# Comma-separated ISO country codes enabled for cloud self-serve onboarding.\n# A country must also have a published regional catalog (see RegionDefaults).\n# UG is enabled with cash/credit-only exception (see cash-credit-only-countries).\napp.selfserve.countries=${APP_SELFSERVE_COUNTRIES:KE,UG}\n# Countries without a local mobile-money rail yet (explicit product exception).\napp.selfserve.cash-credit-only-countries=${APP_SELFSERVE_CASH_CREDIT_ONLY_COUNTRIES:UG}\n\n# Native mobile distribution (deep links, platform store URLs, bundle IDs).\napp.mobile.api-base-url=${APP_MOBILE_API_BASE_URL:https://api.kiosk.ke}\napp.mobile.deep-link-scheme=${APP_MOBILE_DEEP_LINK_SCHEME:kiosk}\napp.mobile.universal-link-scheme=${APP_MOBILE_UNIVERSAL_LINK_SCHEME:https}\napp.mobile.store-links.ios=${APP_MOBILE_STORE_LINK_IOS:}\napp.mobile.store-links.android=${APP_MOBILE_STORE_LINK_ANDROID:}\napp.mobile.platform-bundles.shopper=${APP_MOBILE_BUNDLE_SHOPPER:com.kioskke.shopper}\napp.mobile.platform-bundles.cashier=${APP_MOBILE_BUNDLE_CASHIER:com.kioskke.cashier}\napp.mobile.platform-bundles.grocery=${APP_MOBILE_BUNDLE_GROCERY:com.kioskke.grocery}\napp.mobile.platform-bundles.admin=${APP_MOBILE_BUNDLE_ADMIN:com.kioskke.admin}\napp.mobile.platform-bundles.stock=${APP_MOBILE_BUNDLE_STOCK:com.kioskke.stock}\n# Optional: GitHub Actions dispatch when a tenant requests an App Store build (repository_dispatch).\napp.mobile.publish.github-token=${APP_MOBILE_PUBLISH_GITHUB_TOKEN:}\napp.mobile.publish.github-repo=${APP_MOBILE_PUBLISH_GITHUB_REPO:}\napp.mobile.publish.event-type=${APP_MOBILE_PUBLISH_EVENT_TYPE:mobile-tenant-publish}\n# Shared secret for GitHub Actions → POST /webhooks/mobile-publish/status (openssl rand -base64 32).\napp.mobile.publish.callback-secret=${APP_MOBILE_PUBLISH_CALLBACK_SECRET:}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=${MYSQL_PASSWORD:}\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n# Allow pending lower versions (e.g. V161) when higher versions already ran — common when branches land out of sequence.\nspring.flyway.out-of-order=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=${APP_JWT_SECRET:}\napp.jwt.access-ttl-minutes=60\n\n# HttpOnly refresh token cookie (JSON body omits refreshToken when enabled).\napp.auth.refresh-token-cookie-enabled=true\napp.auth.refresh-cookie-domain=${APP_AUTH_REFRESH_COOKIE_DOMAIN:}\napp.auth.refresh-cookie-secure=${APP_AUTH_REFRESH_COOKIE_SECURE:false}\napp.jwt.refresh-ttl-days=30\n# Revoke refresh when no activity for this many hours (sliding window, updated on refresh).\napp.auth.idle-timeout-hours=12\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.ke,www.kiosk.ke,kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\napp.security.refresh-rate-limit-per-minute=30\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Recurring balance reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Sends WhatsApp/SMS every interval-days to customers with an outstanding tab balance.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.interval-days=3\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.max-count=5\napp.credits.reminders.zone=Africa/Nairobi\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\n# SokoMind (Guide / Brain / Eye) — prefer Super Admin → Platform → SokoMind over env.\n# Env keys are fallbacks when SA has not stored a value yet.\napp.sokomind.enabled=${SOKOMIND_ENABLED:false}\napp.sokomind.guide-enabled=${SOKOMIND_GUIDE_ENABLED:true}\napp.sokomind.brain-enabled=${SOKOMIND_BRAIN_ENABLED:false}\napp.sokomind.eye-enabled=${SOKOMIND_EYE_ENABLED:false}\napp.sokomind.primary-provider=${SOKOMIND_PRIMARY_PROVIDER:openai}\napp.sokomind.default-locale=${SOKOMIND_DEFAULT_LOCALE:en-KE}\napp.sokomind.openai.api-key=${OPENAI_API_KEY:}\napp.sokomind.openai.base-url=${OPENAI_BASE_URL:}\napp.sokomind.openai.mini-model=${SOKOMIND_OPENAI_MINI_MODEL:gpt-4o-mini}\napp.sokomind.openai.smart-model=${SOKOMIND_OPENAI_SMART_MODEL:gpt-4.1}\napp.sokomind.openai.vision-model=${SOKOMIND_OPENAI_VISION_MODEL:gpt-4o}\napp.sokomind.anthropic.api-key=${ANTHROPIC_API_KEY:}\napp.sokomind.anthropic.base-url=${ANTHROPIC_BASE_URL:}\napp.sokomind.anthropic.mini-model=${SOKOMIND_ANTHROPIC_MINI_MODEL:claude-haiku-4-5-20251001}\napp.sokomind.anthropic.smart-model=${SOKOMIND_ANTHROPIC_SMART_MODEL:claude-sonnet-4-5-20250929}\napp.sokomind.deepseek.api-key=${SOKOMIND_DEEPSEEK_API_KEY:${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}}\napp.sokomind.deepseek.base-url=${SOKOMIND_DEEPSEEK_BASE_URL:https://deepseek-v31.p.rapidapi.com/}\napp.sokomind.deepseek.host=${SOKOMIND_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.sokomind.deepseek.model=${SOKOMIND_DEEPSEEK_MODEL:DeepSeek-V3-0324}\napp.sokomind.industry-compare-enabled=${SOKOMIND_INDUSTRY_COMPARE_ENABLED:false}\napp.sokomind.industry-compare-min-twins=${SOKOMIND_INDUSTRY_COMPARE_MIN_TWINS:8}\napp.sokomind.max-tool-calls-per-request=${SOKOMIND_MAX_TOOL_CALLS:8}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# Meta webhook: Callback URL = ${API_PUBLIC_BASE_URL}/webhooks/whatsapp (same verify token in Meta dashboard).\napp.messaging.meta-whatsapp.webhook-verify-token=${WHATSAPP_META_WEBHOOK_VERIFY_TOKEN:}\napp.messaging.meta-whatsapp.app-secret=${WHATSAPP_META_APP_SECRET:}\n# SMS: none (log only) | africas_talking | sozuri | textsms\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\napp.messaging.sms.sozuri-project=${SOZURI_PROJECT:}\napp.messaging.sms.sozuri-api-key=${SOZURI_API_KEY:}\napp.messaging.sms.sozuri-from=${SOZURI_FROM:Sozuri}\napp.messaging.sms.sozuri-type=${SOZURI_TYPE:transactional}\napp.messaging.sms.sozuri-api-url=${SOZURI_API_URL:https://sozuri.net/api/v1/messaging}\n# When SMS is not configured, claim still works by returning the OTP in the JSON (local/dev).\n# Set false in production once Super Admin → Platform → Integrations has Sozuri/TextSMS.\napp.supplier-portal.claim.return-otp-when-stubbed=${APP_SUPPLIER_PORTAL_RETURN_OTP:true}\n# Sozuri callbacks: {API_PUBLIC_BASE_URL}/webhooks/sozuri/inbox and /webhooks/sozuri/delivery\napp.messaging.sms.textsms-partner-id=${TEXTSMS_PARTNER_ID:}\napp.messaging.sms.textsms-api-key=${TEXTSMS_API_KEY:}\napp.messaging.sms.textsms-shortcode=${TEXTSMS_SHORTCODE:}\napp.messaging.sms.textsms-api-url=${TEXTSMS_API_URL:https://sms.textsms.co.ke/api/services/sendsms/}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\n\n# Daily stock audit — random sample of products sold yesterday (branch-scoped manifest).\n# Per-business override: inventory.stocktake.dailyAuditSampleSize (Business settings → Stock take).\napp.inventory.daily-stock-audit.enabled=true\napp.inventory.daily-stock-audit.cron=0 0 6 * * *\napp.inventory.daily-stock-audit.zone=Africa/Nairobi\napp.inventory.daily-stock-audit.sample-size=25\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 4 — async global catalog adopt / promote jobs.\napp.global-catalog.jobs.worker.enabled=true\napp.global-catalog.jobs.worker.poll-interval-ms=3000\napp.global-catalog.jobs.worker.initial-delay-ms=5000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Custom domains — optional env fallback only. Prefer Super Admin → Platform → Domains.\napp.integrations.vercel.token=${VERCEL_TOKEN:}\napp.integrations.vercel.team-id=${VERCEL_TEAM_ID:}\napp.integrations.vercel.project-id=${VERCEL_PROJECT_ID:}\napp.integrations.vercel.api-base-url=${VERCEL_API_BASE_URL:https://api.vercel.com}\n\napp.integrations.hostafrica.api-key=${HOSTAFRICA_API_KEY:}\napp.integrations.hostafrica.api-base-url=${HOSTAFRICA_API_BASE_URL:https://api.hostafrica.com}\napp.integrations.hostafrica.currency=${HOSTAFRICA_CURRENCY:KES}\napp.integrations.hostafrica.billing-stub-enabled=${HOSTAFRICA_BILLING_STUB_ENABLED:true}\napp.integrations.hostafrica.kenyan-tlds=${HOSTAFRICA_KENYAN_TLDS:co.ke,or.ke,me.ke,sc.ke,ac.ke,go.ke,ke}\n\n# Scheduler delay defaults (enable sync from Super Admin Domains page).\napp.integrations.domains.sync-fixed-delay-ms=${DOMAIN_ORDER_SYNC_FIXED_DELAY_MS:60000}\napp.integrations.domains.sync-initial-delay-ms=${DOMAIN_ORDER_SYNC_INITIAL_DELAY_MS:20000}\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}, "b605cdc3c63849c2c61e853e88e7cdfd7645b3d3": {"t": 1786068239.5296447, "n": 21711, "h": "2b0662a445109a07", "c": "spring.application.name=ub\n\n# Redis is for cache/RedisTemplate only; JPA owns all Repository interfaces (no @RedisHash).\nspring.data.redis.repositories.enabled=false\n\n# Hostname appended as {slug}.{suffix} when super-admin creates a tenant without primaryDomain (override via APP_TENANCY_SLUG_DOMAIN_SUFFIX).\napp.tenancy.slug-domain-suffix=${APP_TENANCY_SLUG_DOMAIN_SUFFIX:localhost}\n\n# ISO country codes enabled for cloud self-serve onboarding (* / ALL = every profile\n# in region-profiles.csv). Regional catalogs are optional — tenants fall back to\n# the default catalog when none is published for that country.\napp.selfserve.countries=${APP_SELFSERVE_COUNTRIES:*}\n# Countries without card/mobile-money rails yet (stricter till story). Others still\n# get cash/card/credit until a local rail is configured. Use * / ALL for every\n# country except Kenya (M-Pesa STK).\napp.selfserve.cash-credit-only-countries=${APP_SELFSERVE_CASH_CREDIT_ONLY_COUNTRIES:UG}\n\n# Native mobile distribution (deep links, platform store URLs, bundle IDs).\napp.mobile.api-base-url=${APP_MOBILE_API_BASE_URL:https://api.kiosk.ke}\napp.mobile.deep-link-scheme=${APP_MOBILE_DEEP_LINK_SCHEME:kiosk}\napp.mobile.universal-link-scheme=${APP_MOBILE_UNIVERSAL_LINK_SCHEME:https}\napp.mobile.store-links.ios=${APP_MOBILE_STORE_LINK_IOS:}\napp.mobile.store-links.android=${APP_MOBILE_STORE_LINK_ANDROID:}\napp.mobile.platform-bundles.shopper=${APP_MOBILE_BUNDLE_SHOPPER:com.kioskke.shopper}\napp.mobile.platform-bundles.cashier=${APP_MOBILE_BUNDLE_CASHIER:com.kioskke.cashier}\napp.mobile.platform-bundles.grocery=${APP_MOBILE_BUNDLE_GROCERY:com.kioskke.grocery}\napp.mobile.platform-bundles.admin=${APP_MOBILE_BUNDLE_ADMIN:com.kioskke.admin}\napp.mobile.platform-bundles.stock=${APP_MOBILE_BUNDLE_STOCK:com.kioskke.stock}\n# Optional: GitHub Actions dispatch when a tenant requests an App Store build (repository_dispatch).\napp.mobile.publish.github-token=${APP_MOBILE_PUBLISH_GITHUB_TOKEN:}\napp.mobile.publish.github-repo=${APP_MOBILE_PUBLISH_GITHUB_REPO:}\napp.mobile.publish.event-type=${APP_MOBILE_PUBLISH_EVENT_TYPE:mobile-tenant-publish}\n# Shared secret for GitHub Actions → POST /webhooks/mobile-publish/status (openssl rand -base64 32).\napp.mobile.publish.callback-secret=${APP_MOBILE_PUBLISH_CALLBACK_SECRET:}\n\nserver.port=5050\n# Honor X-Forwarded-Host / X-Forwarded-Proto from the upstream proxy (Next.js BFF rewrites,\n# Cloud Run, Nginx). Without this, requests proxied by the frontend show up with the proxy's\n# host (e.g. kiosk.zelisline.com) and the tenant resolver cannot map them back to the\n# original tenant domain (e.g. tenant.palmart.co.ke). The tenant resolver also falls back\n# to the X-Tenant-Id header, so this is belt-and-suspenders.\nserver.forward-headers-strategy=NATIVE\n\n# MySQL = this is the database we are using\nspring.datasource.url=jdbc:mysql://localhost:3306/ub\nspring.datasource.username=root\nspring.datasource.password=${MYSQL_PASSWORD:}\nspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect\nspring.jpa.properties.hibernate.jdbc.time_zone=UTC\n\nspring.flyway.enabled=true\nspring.flyway.locations=classpath:db/migration\n# Local MySQL only: schema already had objects but no flyway_schema_history. Prefer a fresh database in production.\nspring.flyway.baseline-on-migrate=true\n# Allow pending lower versions (e.g. V161) when higher versions already ran — common when branches land out of sequence.\nspring.flyway.out-of-order=true\n\n# Header-based test principal (Slice 2 bridge). Never enable in production.\napp.security.test-auth.enabled=false\n\n# Slice 3 — JWT + refresh sessions (override in prod).\napp.jwt.secret=${APP_JWT_SECRET:}\napp.jwt.access-ttl-minutes=60\n# Super-admin portal tokens live longer (multi-batch promotes re-host images for a\n# long time) and slide via POST /api/v1/super-admin/auth/refresh.\napp.jwt.super-admin-access-ttl-minutes=240\n\n# HttpOnly refresh token cookie (JSON body omits refreshToken when enabled).\napp.auth.refresh-token-cookie-enabled=true\napp.auth.refresh-cookie-domain=${APP_AUTH_REFRESH_COOKIE_DOMAIN:}\napp.auth.refresh-cookie-secure=${APP_AUTH_REFRESH_COOKIE_SECURE:false}\napp.jwt.refresh-ttl-days=30\n# Revoke refresh when no activity for this many hours (sliding window, updated on refresh).\napp.auth.idle-timeout-hours=12\n\n# Browser-facing frontend (no trailing slash). Set APP_PUBLIC_FRONTEND_BASE_URL in prod.\napp.public.frontend-base-url=${APP_PUBLIC_FRONTEND_BASE_URL:http://localhost:3000}\n# Optional overrides; defaults join frontend-base-url + path (align with NEXT_PUBLIC_APP_BASE_URL).\napp.public.email-verification-url-prefix=${APP_PUBLIC_EMAIL_VERIFICATION_URL_PREFIX:${app.public.frontend-base-url}/verify-email?token=}\napp.public.password-reset-url-prefix=${APP_PUBLIC_PASSWORD_RESET_URL_PREFIX:${app.public.frontend-base-url}/reset-password?token=}\n# Documented for tooling / parity with NEXT_PUBLIC_API_BASE_URL (not all code paths read this yet).\napp.public.api-base-url=${API_PUBLIC_BASE_URL:http://localhost:5050}\n# AES-256 key for payment gateway credentials at rest (base64-encoded 32 bytes). Required in production.\n# Generate: openssl rand -base64 32\napp.payments.encryption-key=${APP_PAYMENTS_ENCRYPTION_KEY:}\n# Hostnames that host the platform itself (API base URL, admin login apex). The\n# tenant resolver bypasses these like localhost — controllers fall back to\n# X-Tenant-Id or to public/non-tenant paths. Comma-separated, no scheme/port.\n# Defaults to the canonical API + apex; override via env to add staging hosts.\napp.tenancy.platform-hosts=${APP_TENANCY_PLATFORM_HOSTS:kiosk.ke,www.kiosk.ke,kiosk.zelisline.com,palmart.co.ke,www.palmart.co.ke}\napp.auth.password-reset-ttl-hours=1\napp.auth.self-signup-enabled=true\n# Role for self-signup when the tenant already has at least one user. The very first user in a tenant always gets owner.\napp.auth.signup-role-key=buyer\n# Optional: when set, POST /api/v1/auth/register may include \"staffInviteToken\" matching this value to assign staff-signup-role-key instead of signup-role-key (first user in tenant is still always owner).\napp.auth.staff-signup-token=${APP_AUTH_STAFF_SIGNUP_TOKEN:}\napp.auth.staff-signup-role-key=viewer\n# true = sign-ups are INVITED until verify-email; resend-verification only applies to those users.\n# false = sign-ups are ACTIVE immediately — resend-verification will not send (no pending verification).\n# Without Resend/Mailgun API keys or SMTP (profile smtp), outbound mail is only logged.\napp.auth.email-verification-required=true\n# When true, register (and resend when a link is issued) expose the verify URL in JSON for dev / broken mail.\napp.auth.return-verification-link-in-register-response=${APP_AUTH_RETURN_VERIFICATION_LINK:false}\napp.auth.email-verification-ttl-hours=48\napp.security.login-rate-limit-per-minute=5\napp.security.refresh-rate-limit-per-minute=30\n# Phase 15 — GET /api/v1/public/** (per client IP + path slug); returns 429 + Retry-After when exceeded.\napp.security.public-storefront-rate-limit-per-minute=120\n# Phase 5 — POST /api/v1/public/credits/** (per client IP); tighter cap because each call may mutate AR state.\napp.security.public-credit-claim-rate-limit-per-minute=10\n\n# Resend (backend only). Prefer over Mailgun when RESEND_API_KEY is set.\n# From must use a domain verified in Resend (or set RESEND_FROM to a full \"Name <addr@domain>\" value).\napp.resend.api-key=${RESEND_API_KEY:}\napp.resend.from=${RESEND_FROM:}\napp.resend.domain=${RESEND_DOMAIN:}\n\n# Mailgun (backend only). API key: MAILGUN_PRIVATE_API_KEY, or API_KEY (Mailgun sample name). Domain: MAILGUN_DOMAIN.\n# Optional explicit From (must be on your verified domain), e.g. UB <noreply@mg.example.com>\napp.mailgun.from=${MAILGUN_FROM:}\napp.mailgun.region=${MAILGUN_REGION:us}\napp.mailgun.domain=${MAILGUN_DOMAIN:}\napp.mailgun.private-api-key=${MAILGUN_PRIVATE_API_KEY:${API_KEY:}}\napp.mailgun.public-validation-key=${MAILGUN_PUBLIC_VALIDATION_KEY:}\napp.mailgun.webhook-signing-key=${MAILGUN_WEBHOOK_SIGNING_KEY:}\n\n# OpenAPI 3.1 — contract snapshot: docs/openapi/phase-1.yaml (see OpenApiExportIT).\n# Public storefront (Phase 15, sessionless): docs/openapi/phase-15-storefront-public.yaml\nspringdoc.api-docs.path=/api/v1/openapi\nspringdoc.swagger-ui.path=/swagger-ui\nspringdoc.api-docs.version=OPENAPI_3_1\nspringdoc.writer-with-default-pretty-printer=true\n\n# Browser origins allowed by Spring Security CORS (exact match to Origin header). Override via env CORS_ALLOWED_ORIGINS in prod.\nCORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,https://palmart.co.ke,https://www.palmart.co.ke\n# Wildcard patterns (subdomains, zelisline admin hosts) — see SecurityConfig. Override via CORS_EXTRA_ORIGIN_PATTERNS.\n# IMPORTANT: https://*.palmart.co.ke does NOT match the apex domain palmart.co.ke (the * requires a subdomain).\n# Include the apex domain explicitly so CORS works even when CORS_ALLOWED_ORIGINS is overridden in prod.\napp.security.cors.extra-origin-patterns=${CORS_EXTRA_ORIGIN_PATTERNS:https://*.palmart.co.ke,https://palmart.co.ke,https://*.kiosk.ke,https://kiosk.ke,https://*.zelisline.com}\n\n# Multipart (catalog image upload to Cloudinary)\nspring.servlet.multipart.max-file-size=12MB\nspring.servlet.multipart.max-request-size=12MB\n\n# Cloudinary — use CLOUDINARY_URL (recommended) or CLOUDINARY_CLOUD_NAME + CLOUDINARY_API_KEY + CLOUDINARY_API_SECRET.\n# When APP_MEDIA_CLOUDINARY_ENABLED is unset, uploads auto-enable if all three values are non-blank (after URL parse).\n# Opt out explicitly with APP_MEDIA_CLOUDINARY_ENABLED=false.\napp.media.cloudinary.enabled=${APP_MEDIA_CLOUDINARY_ENABLED:true}\napp.media.cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:}\napp.media.cloudinary.api-key=${CLOUDINARY_API_KEY:}\napp.media.cloudinary.api-secret=${CLOUDINARY_API_SECRET:}\n\n# Phase 5 — M-Pesa STK simulation webhook (leave empty to reject completion callbacks).\napp.payments.mpesa-stk-simulate-secret=${MPESA_STK_SIMULATE_SECRET:}\n# Local STK rows still pending after this many seconds are marked failed so cashier/storefront can retry (KopoKopo may still block longer).\napp.payments.stk.stale-pending-seconds=${APP_PAYMENTS_STK_STALE_PENDING_SECONDS:30}\n# Supplier Send Money rows still pending after this are marked failed so the pay drawer stops polling.\napp.payments.send-money.stale-pending-seconds=${APP_PAYMENTS_SEND_MONEY_STALE_PENDING_SECONDS:180}\n# Recurring balance reminder sweep (Phase 5 Slice 6). Enable with app.credits.reminders.enabled=true.\n# Cron may run daily; each customer is re-reminded at most once per interval-days (default 3).\n# After-sale credit receipts always send; they also stamp last_balance_reminder_at so the\n# sweep / staff Remind button won't re-blast within interval-days.\napp.credits.reminders.enabled=false\napp.credits.reminders.cron=0 0 8 * * *\napp.credits.reminders.interval-days=3\napp.credits.reminders.min-balance=1.00\napp.credits.reminders.max-count=5\napp.credits.reminders.zone=Africa/Nairobi\n\n# Credit tab sale reminders — per-tenant toggles live in business_credit_settings (admin UI).\n# Env vars below are optional fallbacks when a tenant has not saved keys in admin yet.\napp.messaging.credit-sale-reminder.payment-account-url=${APP_CREDIT_SALE_REMINDER_PAYMENT_URL:https://palmart.co.ke}\n# Product description AI (DeepSeek via RapidAPI)\napp.catalog.ai.deepseek.api-key=${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}\napp.catalog.ai.deepseek.host=${RAPIDAPI_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.catalog.ai.deepseek.url=${RAPIDAPI_DEEPSEEK_URL:https://deepseek-v31.p.rapidapi.com/}\napp.catalog.ai.deepseek.model=${RAPIDAPI_DEEPSEEK_MODEL:DeepSeek-V3-0324}\n\n# SokoMind (Guide / Brain / Eye) — prefer Super Admin → Platform → SokoMind over env.\n# Env keys are fallbacks when SA has not stored a value yet.\napp.sokomind.enabled=${SOKOMIND_ENABLED:false}\napp.sokomind.guide-enabled=${SOKOMIND_GUIDE_ENABLED:true}\napp.sokomind.brain-enabled=${SOKOMIND_BRAIN_ENABLED:false}\napp.sokomind.eye-enabled=${SOKOMIND_EYE_ENABLED:false}\napp.sokomind.primary-provider=${SOKOMIND_PRIMARY_PROVIDER:openai}\napp.sokomind.default-locale=${SOKOMIND_DEFAULT_LOCALE:en-KE}\napp.sokomind.openai.api-key=${OPENAI_API_KEY:}\napp.sokomind.openai.base-url=${OPENAI_BASE_URL:}\napp.sokomind.openai.mini-model=${SOKOMIND_OPENAI_MINI_MODEL:gpt-4o-mini}\napp.sokomind.openai.smart-model=${SOKOMIND_OPENAI_SMART_MODEL:gpt-4.1}\napp.sokomind.openai.vision-model=${SOKOMIND_OPENAI_VISION_MODEL:gpt-4o}\napp.sokomind.anthropic.api-key=${ANTHROPIC_API_KEY:}\napp.sokomind.anthropic.base-url=${ANTHROPIC_BASE_URL:}\napp.sokomind.anthropic.mini-model=${SOKOMIND_ANTHROPIC_MINI_MODEL:claude-haiku-4-5-20251001}\napp.sokomind.anthropic.smart-model=${SOKOMIND_ANTHROPIC_SMART_MODEL:claude-sonnet-4-5-20250929}\napp.sokomind.deepseek.api-key=${SOKOMIND_DEEPSEEK_API_KEY:${RAPIDAPI_DEEPSEEK_KEY:${RAPIDAPI_KEY:}}}\napp.sokomind.deepseek.base-url=${SOKOMIND_DEEPSEEK_BASE_URL:https://deepseek-v31.p.rapidapi.com/}\napp.sokomind.deepseek.host=${SOKOMIND_DEEPSEEK_HOST:deepseek-v31.p.rapidapi.com}\napp.sokomind.deepseek.model=${SOKOMIND_DEEPSEEK_MODEL:DeepSeek-V3-0324}\napp.sokomind.industry-compare-enabled=${SOKOMIND_INDUSTRY_COMPARE_ENABLED:false}\napp.sokomind.industry-compare-min-twins=${SOKOMIND_INDUSTRY_COMPARE_MIN_TWINS:8}\napp.sokomind.max-tool-calls-per-request=${SOKOMIND_MAX_TOOL_CALLS:8}\n\napp.messaging.rapid-api-whatsapp.api-key=${RAPIDAPI_KEY:}\napp.messaging.rapid-api-whatsapp.host=${RAPIDAPI_WHATSAPP_HOST:whatsapp-osint.p.rapidapi.com}\napp.messaging.rapid-api-whatsapp.lookup-url=${RAPIDAPI_WHATSAPP_LOOKUP_URL:https://whatsapp-osint.p.rapidapi.com/bizos}\napp.messaging.meta-whatsapp.access-token=${WHATSAPP_META_ACCESS_TOKEN:}\napp.messaging.meta-whatsapp.phone-number-id=${WHATSAPP_META_PHONE_NUMBER_ID:}\napp.messaging.meta-whatsapp.graph-version=${WHATSAPP_META_GRAPH_VERSION:v25.0}\n# Meta webhook: Callback URL = ${API_PUBLIC_BASE_URL}/webhooks/whatsapp (same verify token in Meta dashboard).\napp.messaging.meta-whatsapp.webhook-verify-token=${WHATSAPP_META_WEBHOOK_VERIFY_TOKEN:}\napp.messaging.meta-whatsapp.app-secret=${WHATSAPP_META_APP_SECRET:}\n# SMS: none (log only) | africas_talking | sozuri | textsms\napp.messaging.sms.provider=${SMS_PROVIDER:none}\napp.messaging.sms.africas-talking-username=${AFRICAS_TALKING_USERNAME:}\napp.messaging.sms.africas-talking-api-key=${AFRICAS_TALKING_API_KEY:}\napp.messaging.sms.sozuri-project=${SOZURI_PROJECT:}\napp.messaging.sms.sozuri-api-key=${SOZURI_API_KEY:}\napp.messaging.sms.sozuri-from=${SOZURI_FROM:Sozuri}\napp.messaging.sms.sozuri-type=${SOZURI_TYPE:transactional}\napp.messaging.sms.sozuri-api-url=${SOZURI_API_URL:https://sozuri.net/api/v1/messaging}\n# When SMS is not configured, claim still works by returning the OTP in the JSON (local/dev).\n# Set false in production once Super Admin → Platform → Integrations has Sozuri/TextSMS.\napp.supplier-portal.claim.return-otp-when-stubbed=${APP_SUPPLIER_PORTAL_RETURN_OTP:true}\n# Sozuri callbacks: {API_PUBLIC_BASE_URL}/webhooks/sozuri/inbox and /webhooks/sozuri/delivery\napp.messaging.sms.textsms-partner-id=${TEXTSMS_PARTNER_ID:}\napp.messaging.sms.textsms-api-key=${TEXTSMS_API_KEY:}\napp.messaging.sms.textsms-shortcode=${TEXTSMS_SHORTCODE:}\napp.messaging.sms.textsms-api-url=${TEXTSMS_API_URL:https://sms.textsms.co.ke/api/services/sendsms/}\n\n# Phase 6 — recurring expense generation/posting sweep.\napp.finance.recurring-expenses.enabled=true\napp.finance.recurring-expenses.cron=0 5 2 * * *\n\n# Phase 7 Slice 1 — MV summary refresh (disabled by default; see ReportingRefreshScheduler).\napp.reporting.refresh.enabled=false\napp.reporting.refresh.cron=0 5 * * * *\napp.reporting.refresh.zone=UTC\n\n# Phase B — notification outbox, batching, delivery retries (workers off by default in dev).\napp.notifications.outbox.enabled=true\napp.notifications.worker.enabled=false\napp.notifications.worker.fixed-delay-ms=5000\napp.notifications.worker.initial-delay-ms=8000\napp.notifications.stock-low.batch-window-minutes=15\napp.notifications.stock-low.flusher.enabled=false\napp.notifications.stock-low.flusher.fixed-delay-ms=60000\napp.notifications.stock-low.flusher.initial-delay-ms=30000\napp.notifications.delivery.worker.enabled=false\napp.notifications.delivery.worker.fixed-delay-ms=20000\napp.notifications.delivery.worker.initial-delay-ms=10000\napp.notifications.delivery.max-attempts=8\napp.notifications.daily-digest.enabled=false\napp.notifications.daily-digest.cron=0 0 20 * * *\napp.notifications.daily-digest.zone=Africa/Nairobi\n\n# Daily stock audit — random sample of products sold yesterday (branch-scoped manifest).\n# Per-business override: inventory.stocktake.dailyAuditSampleSize (Business settings → Stock take).\napp.inventory.daily-stock-audit.enabled=true\napp.inventory.daily-stock-audit.cron=0 0 6 * * *\napp.inventory.daily-stock-audit.zone=Africa/Nairobi\napp.inventory.daily-stock-audit.sample-size=25\napp.notifications.insights.enabled=false\napp.notifications.insights.zone=Africa/Nairobi\napp.notifications.insights.abandoned-cart.cron=0 0 9 * * *\napp.notifications.insights.peak-hours.cron=0 0 8 * * *\napp.notifications.insights.top-products.cron=0 0 7 * * MON\napp.notifications.abandoned-cart.stale-hours=24\napp.notifications.win-back.cron=0 0 10 * * MON\napp.notifications.win-back.inactive-days=30\napp.notifications.promo-campaigns.enabled=false\napp.notifications.promo-campaigns.poll-ms=60000\n\n# Phase C — Web Push (generate keys: npx web-push generate-vapid-keys)\n# After STK success, optionally skip manual confirm step (sends order.confirmed immediately).\napp.storefront.web-orders.auto-confirm-on-paid=false\n\napp.notifications.fcm.enabled=false\napp.notifications.fcm.server-key=${FCM_SERVER_KEY:}\napp.notifications.web-push.enabled=false\napp.notifications.web-push.vapid-public-key=${VAPID_PUBLIC_KEY:}\napp.notifications.web-push.vapid-private-key=${VAPID_PRIVATE_KEY:}\napp.notifications.web-push.subject=mailto:notifications@palmart.co.ke\n\n# Phase 7 Slice 6 — overdue AP inbox notifications (disabled by default).\napp.phase7.notifications.enabled=false\napp.phase7.notifications.overdue-ap.cron=0 15 8 * * *\napp.phase7.notifications.zone=UTC\n\n# Phase 8 Slice 1 — integration API keys (per-key sliding window, requests/minute).\napp.integrations.api-key.requests-per-minute=120\n\n# Phase 8 Slice 2 — outbound webhooks (transactional enqueue + async HTTP). Scheduler off by default.\napp.integrations.webhook.delivery.enabled=false\napp.integrations.webhook.delivery.fixed-delay-ms=15000\napp.integrations.webhook.delivery.initial-delay-ms=5000\napp.integrations.webhook.delivery.http-timeout-seconds=15\napp.integrations.webhook.delivery.max-attempts=12\n# Phase 8 Slice 3 — async CSV import jobs (worker off by default for dev/tests).\napp.integrations.import.jobs.storage-dir=${java.io.tmpdir}/ub-import-jobs\napp.integrations.import.jobs.worker.enabled=false\napp.integrations.import.jobs.worker.poll-interval-ms=4000\napp.integrations.import.jobs.worker.initial-delay-ms=8000\n\n# Phase 4 — async global catalog adopt / promote jobs.\napp.global-catalog.jobs.worker.enabled=true\napp.global-catalog.jobs.worker.poll-interval-ms=3000\napp.global-catalog.jobs.worker.initial-delay-ms=5000\n\n# Phase 8 Slice 4 — encrypted DB backup (mysqldump / pg_dump → AES-GCM → S3 or local dir). Disabled by default.\napp.integrations.backup.enabled=false\napp.integrations.backup.encryption.passphrase=${BACKUP_ENCRYPTION_PASSPHRASE:}\napp.integrations.backup.local-dir=${BACKUP_LOCAL_DIR:}\napp.integrations.backup.object-prefix=db-backups\napp.integrations.backup.retention-days=30\napp.integrations.backup.scheduler.enabled=false\napp.integrations.backup.scheduler.cron=0 0 3 * * *\napp.integrations.backup.scheduler.zone=UTC\napp.integrations.backup.s3.bucket=${BACKUP_S3_BUCKET:${STORAGE_BUCKET:}}\napp.integrations.backup.s3.region=${AWS_REGION:eu-west-1}\napp.integrations.backup.s3.endpoint=${STORAGE_ENDPOINT:}\napp.integrations.backup.s3.access-key=${STORAGE_ACCESS_KEY:}\napp.integrations.backup.s3.secret-key=${STORAGE_SECRET_KEY:}\napp.integrations.backup.s3.path-style=${STORAGE_PATH_STYLE:false}\napp.integrations.backup.s3.default-credentials-chain=false\n\n# Phase 9 Slice 0 — WebSocket realtime layer (ticket TTL, connection limits).\napp.realtime.ticket.ttl-seconds=60\napp.realtime.max-connections-per-user=5\napp.realtime.max-connections-per-business=50\n\n# Custom domains — optional env fallback only. Prefer Super Admin → Platform → Domains.\napp.integrations.vercel.token=${VERCEL_TOKEN:}\napp.integrations.vercel.team-id=${VERCEL_TEAM_ID:}\napp.integrations.vercel.project-id=${VERCEL_PROJECT_ID:}\napp.integrations.vercel.api-base-url=${VERCEL_API_BASE_URL:https://api.vercel.com}\n\napp.integrations.hostafrica.api-key=${HOSTAFRICA_API_KEY:}\napp.integrations.hostafrica.api-base-url=${HOSTAFRICA_API_BASE_URL:https://api.hostafrica.com}\napp.integrations.hostafrica.currency=${HOSTAFRICA_CURRENCY:KES}\napp.integrations.hostafrica.billing-stub-enabled=${HOSTAFRICA_BILLING_STUB_ENABLED:true}\napp.integrations.hostafrica.kenyan-tlds=${HOSTAFRICA_KENYAN_TLDS:co.ke,or.ke,me.ke,sc.ke,ac.ke,go.ke,ke}\n\n# Scheduler delay defaults (enable sync from Super Admin Domains page).\napp.integrations.domains.sync-fixed-delay-ms=${DOMAIN_ORDER_SYNC_FIXED_DELAY_MS:60000}\napp.integrations.domains.sync-initial-delay-ms=${DOMAIN_ORDER_SYNC_INITIAL_DELAY_MS:20000}\n\n# Phase 8 Slice 6 — hardening (API-key brute-force throttle per IP; webhook SSRF on subscription URL).\napp.integrations.api-key.invalid-attempts-per-minute-per-ip=40\napp.integrations.webhook.allow-loopback-and-rfc1918-targets=false\napp.integrations.webhook.ssrf.resolve-hostnames=true\napp.integrations.webhook.ssrf.resolve-timeout-ms=1500\n"}}, "src/main/java/zelisline/ub/platform/domain/PlatformSokoMindSettings.java": {"d452a1d4161ea83c70cf8212b3c48e5a499096b3": {"t": 1785745699.4777834, "n": 3305, "h": "beb410e853e26cf7", "c": "package zelisline.ub.platform.domain;\n\nimport java.time.Instant;\n\nimport jakarta.persistence.Column;\nimport jakarta.persistence.Entity;\nimport jakarta.persistence.Id;\nimport jakarta.persistence.Table;\nimport lombok.Getter;\nimport lombok.Setter;\n\n@Entity\n@Table(name = \"platform_sokomind_settings\")\n@Getter\n@Setter\npublic class PlatformSokoMindSettings {\n\n public static final String SINGLETON_ID = \"00000000-0000-0000-0000-000000000001\";\n\n public static final String PROVIDER_OPENAI = \"openai\";\n public static final String PROVIDER_ANTHROPIC = \"anthropic\";\n public static final String PROVIDER_DEEPSEEK = \"deepseek\";\n public static final String PROVIDER_RAPIDAPI_DEEPSEEK = \"rapidapi_deepseek\";\n\n @Id\n @Column(length = 36, nullable = false)\n private String id;\n\n @Column(name = \"sokomind_enabled\", nullable = false)\n private boolean sokomindEnabled = false;\n\n @Column(name = \"guide_enabled\", nullable = false)\n private boolean guideEnabled = true;\n\n @Column(name = \"brain_enabled\", nullable = false)\n private boolean brainEnabled = false;\n\n @Column(name = \"eye_enabled\", nullable = false)\n private boolean eyeEnabled = false;\n\n @Column(name = \"primary_provider\", length = 32, nullable = false)\n private String primaryProvider = PROVIDER_OPENAI;\n\n @Column(name = \"default_locale\", length = 16, nullable = false)\n private String defaultLocale = \"en-KE\";\n\n @Column(name = \"openai_api_key_enc\", columnDefinition = \"TEXT\")\n private String openaiApiKeyEnc;\n\n @Column(name = \"openai_base_url\", length = 512)\n private String openaiBaseUrl;\n\n @Column(name = \"openai_mini_model\", length = 128)\n private String openaiMiniModel;\n\n @Column(name = \"openai_smart_model\", length = 128)\n private String openaiSmartModel;\n\n @Column(name = \"openai_vision_model\", length = 128)\n private String openaiVisionModel;\n\n @Column(name = \"anthropic_api_key_enc\", columnDefinition = \"TEXT\")\n private String anthropicApiKeyEnc;\n\n @Column(name = \"anthropic_base_url\", length = 512)\n private String anthropicBaseUrl;\n\n @Column(name = \"anthropic_mini_model\", length = 128)\n private String anthropicMiniModel;\n\n @Column(name = \"anthropic_smart_model\", length = 128)\n private String anthropicSmartModel;\n\n @Column(name = \"deepseek_api_key_enc\", columnDefinition = \"TEXT\")\n private String deepseekApiKeyEnc;\n\n @Column(name = \"deepseek_base_url\", length = 512)\n private String deepseekBaseUrl;\n\n @Column(name = \"deepseek_host\", length = 255)\n private String deepseekHost;\n\n @Column(name = \"deepseek_model\", length = 128)\n private String deepseekModel;\n\n @Column(name = \"industry_compare_enabled\", nullable = false)\n private boolean industryCompareEnabled = false;\n\n @Column(name = \"industry_compare_min_twins\", nullable = false)\n private int industryCompareMinTwins = 8;\n\n @Column(name = \"daily_token_budget_per_tenant\")\n private Integer dailyTokenBudgetPerTenant;\n\n @Column(name = \"max_tool_calls_per_request\", nullable = false)\n private int maxToolCallsPerRequest = 8;\n\n @Column(name = \"system_prompt_extra\", columnDefinition = \"TEXT\")\n private String systemPromptExtra;\n\n @Column(name = \"updated_at\", nullable = false)\n private Instant updatedAt = Instant.now();\n}\n"}, "2147a16343ad0c606e15684e98cb097ba3e49be9": {"t": 1785752685.8216767, "n": 3305, "h": "beb410e853e26cf7", "c": "package zelisline.ub.platform.domain;\n\nimport java.time.Instant;\n\nimport jakarta.persistence.Column;\nimport jakarta.persistence.Entity;\nimport jakarta.persistence.Id;\nimport jakarta.persistence.Table;\nimport lombok.Getter;\nimport lombok.Setter;\n\n@Entity\n@Table(name = \"platform_sokomind_settings\")\n@Getter\n@Setter\npublic class PlatformSokoMindSettings {\n\n public static final String SINGLETON_ID = \"00000000-0000-0000-0000-000000000001\";\n\n public static final String PROVIDER_OPENAI = \"openai\";\n public static final String PROVIDER_ANTHROPIC = \"anthropic\";\n public static final String PROVIDER_DEEPSEEK = \"deepseek\";\n public static final String PROVIDER_RAPIDAPI_DEEPSEEK = \"rapidapi_deepseek\";\n\n @Id\n @Column(length = 36, nullable = false)\n private String id;\n\n @Column(name = \"sokomind_enabled\", nullable = false)\n private boolean sokomindEnabled = false;\n\n @Column(name = \"guide_enabled\", nullable = false)\n private boolean guideEnabled = true;\n\n @Column(name = \"brain_enabled\", nullable = false)\n private boolean brainEnabled = false;\n\n @Column(name = \"eye_enabled\", nullable = false)\n private boolean eyeEnabled = false;\n\n @Column(name = \"primary_provider\", length = 32, nullable = false)\n private String primaryProvider = PROVIDER_OPENAI;\n\n @Column(name = \"default_locale\", length = 16, nullable = false)\n private String defaultLocale = \"en-KE\";\n\n @Column(name = \"openai_api_key_enc\", columnDefinition = \"TEXT\")\n private String openaiApiKeyEnc;\n\n @Column(name = \"openai_base_url\", length = 512)\n private String openaiBaseUrl;\n\n @Column(name = \"openai_mini_model\", length = 128)\n private String openaiMiniModel;\n\n @Column(name = \"openai_smart_model\", length = 128)\n private String openaiSmartModel;\n\n @Column(name = \"openai_vision_model\", length = 128)\n private String openaiVisionModel;\n\n @Column(name = \"anthropic_api_key_enc\", columnDefinition = \"TEXT\")\n private String anthropicApiKeyEnc;\n\n @Column(name = \"anthropic_base_url\", length = 512)\n private String anthropicBaseUrl;\n\n @Column(name = \"anthropic_mini_model\", length = 128)\n private String anthropicMiniModel;\n\n @Column(name = \"anthropic_smart_model\", length = 128)\n private String anthropicSmartModel;\n\n @Column(name = \"deepseek_api_key_enc\", columnDefinition = \"TEXT\")\n private String deepseekApiKeyEnc;\n\n @Column(name = \"deepseek_base_url\", length = 512)\n private String deepseekBaseUrl;\n\n @Column(name = \"deepseek_host\", length = 255)\n private String deepseekHost;\n\n @Column(name = \"deepseek_model\", length = 128)\n private String deepseekModel;\n\n @Column(name = \"industry_compare_enabled\", nullable = false)\n private boolean industryCompareEnabled = false;\n\n @Column(name = \"industry_compare_min_twins\", nullable = false)\n private int industryCompareMinTwins = 8;\n\n @Column(name = \"daily_token_budget_per_tenant\")\n private Integer dailyTokenBudgetPerTenant;\n\n @Column(name = \"max_tool_calls_per_request\", nullable = false)\n private int maxToolCallsPerRequest = 8;\n\n @Column(name = \"system_prompt_extra\", columnDefinition = \"TEXT\")\n private String systemPromptExtra;\n\n @Column(name = \"updated_at\", nullable = false)\n private Instant updatedAt = Instant.now();\n}\n"}, "e95fb67da05a7a50e2e210d5ce6f42352845e5ff": {"t": 1786067441.4416711, "n": 3305, "h": "beb410e853e26cf7", "c": "package zelisline.ub.platform.domain;\n\nimport java.time.Instant;\n\nimport jakarta.persistence.Column;\nimport jakarta.persistence.Entity;\nimport jakarta.persistence.Id;\nimport jakarta.persistence.Table;\nimport lombok.Getter;\nimport lombok.Setter;\n\n@Entity\n@Table(name = \"platform_sokomind_settings\")\n@Getter\n@Setter\npublic class PlatformSokoMindSettings {\n\n public static final String SINGLETON_ID = \"00000000-0000-0000-0000-000000000001\";\n\n public static final String PROVIDER_OPENAI = \"openai\";\n public static final String PROVIDER_ANTHROPIC = \"anthropic\";\n public static final String PROVIDER_DEEPSEEK = \"deepseek\";\n public static final String PROVIDER_RAPIDAPI_DEEPSEEK = \"rapidapi_deepseek\";\n\n @Id\n @Column(length = 36, nullable = false)\n private String id;\n\n @Column(name = \"sokomind_enabled\", nullable = false)\n private boolean sokomindEnabled = false;\n\n @Column(name = \"guide_enabled\", nullable = false)\n private boolean guideEnabled = true;\n\n @Column(name = \"brain_enabled\", nullable = false)\n private boolean brainEnabled = false;\n\n @Column(name = \"eye_enabled\", nullable = false)\n private boolean eyeEnabled = false;\n\n @Column(name = \"primary_provider\", length = 32, nullable = false)\n private String primaryProvider = PROVIDER_OPENAI;\n\n @Column(name = \"default_locale\", length = 16, nullable = false)\n private String defaultLocale = \"en-KE\";\n\n @Column(name = \"openai_api_key_enc\", columnDefinition = \"TEXT\")\n private String openaiApiKeyEnc;\n\n @Column(name = \"openai_base_url\", length = 512)\n private String openaiBaseUrl;\n\n @Column(name = \"openai_mini_model\", length = 128)\n private String openaiMiniModel;\n\n @Column(name = \"openai_smart_model\", length = 128)\n private String openaiSmartModel;\n\n @Column(name = \"openai_vision_model\", length = 128)\n private String openaiVisionModel;\n\n @Column(name = \"anthropic_api_key_enc\", columnDefinition = \"TEXT\")\n private String anthropicApiKeyEnc;\n\n @Column(name = \"anthropic_base_url\", length = 512)\n private String anthropicBaseUrl;\n\n @Column(name = \"anthropic_mini_model\", length = 128)\n private String anthropicMiniModel;\n\n @Column(name = \"anthropic_smart_model\", length = 128)\n private String anthropicSmartModel;\n\n @Column(name = \"deepseek_api_key_enc\", columnDefinition = \"TEXT\")\n private String deepseekApiKeyEnc;\n\n @Column(name = \"deepseek_base_url\", length = 512)\n private String deepseekBaseUrl;\n\n @Column(name = \"deepseek_host\", length = 255)\n private String deepseekHost;\n\n @Column(name = \"deepseek_model\", length = 128)\n private String deepseekModel;\n\n @Column(name = \"industry_compare_enabled\", nullable = false)\n private boolean industryCompareEnabled = false;\n\n @Column(name = \"industry_compare_min_twins\", nullable = false)\n private int industryCompareMinTwins = 8;\n\n @Column(name = \"daily_token_budget_per_tenant\")\n private Integer dailyTokenBudgetPerTenant;\n\n @Column(name = \"max_tool_calls_per_request\", nullable = false)\n private int maxToolCallsPerRequest = 8;\n\n @Column(name = \"system_prompt_extra\", columnDefinition = \"TEXT\")\n private String systemPromptExtra;\n\n @Column(name = \"updated_at\", nullable = false)\n private Instant updatedAt = Instant.now();\n}\n"}, "b605cdc3c63849c2c61e853e88e7cdfd7645b3d3": {"t": 1786068302.8596709, "n": 3305, "h": "beb410e853e26cf7", "c": "package zelisline.ub.platform.domain;\n\nimport java.time.Instant;\n\nimport jakarta.persistence.Column;\nimport jakarta.persistence.Entity;\nimport jakarta.persistence.Id;\nimport jakarta.persistence.Table;\nimport lombok.Getter;\nimport lombok.Setter;\n\n@Entity\n@Table(name = \"platform_sokomind_settings\")\n@Getter\n@Setter\npublic class PlatformSokoMindSettings {\n\n public static final String SINGLETON_ID = \"00000000-0000-0000-0000-000000000001\";\n\n public static final String PROVIDER_OPENAI = \"openai\";\n public static final String PROVIDER_ANTHROPIC = \"anthropic\";\n public static final String PROVIDER_DEEPSEEK = \"deepseek\";\n public static final String PROVIDER_RAPIDAPI_DEEPSEEK = \"rapidapi_deepseek\";\n\n @Id\n @Column(length = 36, nullable = false)\n private String id;\n\n @Column(name = \"sokomind_enabled\", nullable = false)\n private boolean sokomindEnabled = false;\n\n @Column(name = \"guide_enabled\", nullable = false)\n private boolean guideEnabled = true;\n\n @Column(name = \"brain_enabled\", nullable = false)\n private boolean brainEnabled = false;\n\n @Column(name = \"eye_enabled\", nullable = false)\n private boolean eyeEnabled = false;\n\n @Column(name = \"primary_provider\", length = 32, nullable = false)\n private String primaryProvider = PROVIDER_OPENAI;\n\n @Column(name = \"default_locale\", length = 16, nullable = false)\n private String defaultLocale = \"en-KE\";\n\n @Column(name = \"openai_api_key_enc\", columnDefinition = \"TEXT\")\n private String openaiApiKeyEnc;\n\n @Column(name = \"openai_base_url\", length = 512)\n private String openaiBaseUrl;\n\n @Column(name = \"openai_mini_model\", length = 128)\n private String openaiMiniModel;\n\n @Column(name = \"openai_smart_model\", length = 128)\n private String openaiSmartModel;\n\n @Column(name = \"openai_vision_model\", length = 128)\n private String openaiVisionModel;\n\n @Column(name = \"anthropic_api_key_enc\", columnDefinition = \"TEXT\")\n private String anthropicApiKeyEnc;\n\n @Column(name = \"anthropic_base_url\", length = 512)\n private String anthropicBaseUrl;\n\n @Column(name = \"anthropic_mini_model\", length = 128)\n private String anthropicMiniModel;\n\n @Column(name = \"anthropic_smart_model\", length = 128)\n private String anthropicSmartModel;\n\n @Column(name = \"deepseek_api_key_enc\", columnDefinition = \"TEXT\")\n private String deepseekApiKeyEnc;\n\n @Column(name = \"deepseek_base_url\", length = 512)\n private String deepseekBaseUrl;\n\n @Column(name = \"deepseek_host\", length = 255)\n private String deepseekHost;\n\n @Column(name = \"deepseek_model\", length = 128)\n private String deepseekModel;\n\n @Column(name = \"industry_compare_enabled\", nullable = false)\n private boolean industryCompareEnabled = false;\n\n @Column(name = \"industry_compare_min_twins\", nullable = false)\n private int industryCompareMinTwins = 8;\n\n @Column(name = \"daily_token_budget_per_tenant\")\n private Integer dailyTokenBudgetPerTenant;\n\n @Column(name = \"max_tool_calls_per_request\", nullable = false)\n private int maxToolCallsPerRequest = 8;\n\n @Column(name = \"system_prompt_extra\", columnDefinition = \"TEXT\")\n private String systemPromptExtra;\n\n @Column(name = \"updated_at\", nullable = false)\n private Instant updatedAt = Instant.now();\n}\n"}}}} |