Skip to main content
Transparency

Platform Changelog

We are building the most reliable procurement platform in South Africa. Follow our journey of continuous improvement, data quality, and transparency.

2026-06-11
Company Intelligence
Company profiles — fixed crash on claimed company intelligence pages

Company intelligence pages for claimed company profiles were crashing with a Prisma "Unknown field" error. The Company model was missing the contactEmail, contactPhone, and website fields that the claim-owner enrichment section was querying. These fields are now available on the Company model, and claimed company profiles render the owner's contact information correctly.

Key Changes

  • Added 3 optional fields to the Company model: contactEmail, contactPhone, website. Required a database schema push to sync the new columns.
  • Claimed company intelligence pages (with an approved company claim) no longer crash when rendering the claim-owner enrichment section.
  • The claim-owner enrichment section now correctly surfaces the company owner's email, phone, and website when populated.

Platform Impact

The ~R1,500 company claim flow now completes successfully — users who have claimed and been approved no longer see a crash on their company intelligence profile. The owner's contact details, capabilities, B-BBEE level, and profile text are all displayed correctly.

2026-06-11
SEO & AI Visibility
AI Discovery & Citation Stack — edge markdown mirrors, AI info API, cost guardrail

70+ public routes now serve AI-optimized markdown at /_ai/* mirror paths, with 16+ bot families detected at the middleware edge. A new AI Info API, 4 JSON Feeds + 4 JSON sitemaps, build-synced llms.txt/llms-full.txt, publisher citation JSON-LD blocks, 4-tier cache policy, AI-aware rate limiting (60/10 rpm tiers), and a daily cost guardrail cron complete the AI Discovery & Citation Optimization Stack. Prompt v2.0 (657 lines, 7-layer architecture) tells AI bots what Tenders-SA is, what data it provides, and how to cite it.

Key Changes

  • 70+ public routes now serve AI-optimized markdown at /_ai/* mirror paths. Covers tenders by ID, province, category, industry, glossary, blog, legislation, procurement tools, BBBEE compliance, and 15+ entity types via generic formatters. Each mirror response includes attribution + a single contextual CTA.
  • 16+ bot families detected at middleware edge: Bytespider, ChatGPT-User, Claude-Web, Gemini, PerplexityBot, Amazonbot, cohere-ai, ExaBot, FacebookBot, GoogleOther, ImagesiftBot, PetalBot, Scrapy, YouBot, YandexBot, bingbot, plus 0-known/generic AI crawlers. Abusive scrapers get 10 rpm; well-behaved bots get 60 rpm.
  • New public AI Info API at /api/v1/ai-info returning the full entity taxonomy (41 types), JSON Feed URLs, and LLMs.txt references. Static snapshot synced to public/ai-info.json at build time.
  • 4 JSON Feeds (tenders.json, awards.json) + 4 JSON sitemaps (tenders.json, awards.json, pages.json, entities.json) for AI aggregators and feed readers.
  • llms.txt and llms-full.txt regenerated from canonical sources via sync-llms-files.ts at build time. llms.txt references feeds, AI Info API, robots.txt, sitemap. llms-full.txt provides inline templates for every mirror route.
  • robots.txt updated: CCBot, GPTBot, Claude-Web, Diffbot, ImagesiftBot disallowed from /api/, /_next/, /tools/, /dashboard/, /profile/. Well-behaved AI bots (ChatGPT-User, PerplexityBot, cohere-ai, ExaBot, GoogleExtended, Amazonbot) allowed full access.
  • Publisher citation JSON-LD block emitted on all mirror responses: { publisher, isBasedOn?, sourceOrganization? } under @context: "https://schema.org", linking back to Tenders-SA.org.
  • 4-tier cache policy: short-lived entities (1-10 min), stable references (1-24 h), aggregated lists (1-6 h), volatile data (1 min) — all with stale-while-revalidate.
  • Cost guardrail cron at /api/cron/ai-discovery-cost monitoring daily mirror bandwidth (default threshold: 50 GB). Sends admin alert via existing email template infrastructure.
  • Rate limiter with AI-specific tiers: 60 rpm for well-behaved bots, 10 rpm for abusive/unknown — thin shim wrapping the existing frozen Tier 2 rate limiter.
  • Publisher-block pure-function component for JSON-LD citation attribution.
  • Prompt v2.0 (prompts/bots/prompt.md, 657 lines) — 7-layer architecture: system identity, engagement principles, entity taxonomy (41 types), platform capabilities, behavior rules per bot family, promotion CTAs, universal footer with Tenders-SA.org branding.
  • Spec package at .kiro/specs/ai-discovery-citation-stack/ — spec, design (1,145 lines TypeScript), 64-task breakdown, Impact Assessment for middleware, reconciled VALIDATION.md.

Platform Impact

Tenders-SA is now fully discoverable and citable by all major AI platforms. AI bots that previously saw JavaScript-heavy SPAs or 404s now receive clean markdown with attribution. The platform's entity taxonomy (41 types), 70+ content routes, and 4 JSON feeds are exposed via the AI Info API, JSON sitemaps, and llms.txt — making the site a first-class data source for AI training and real-time inference. The cost guardrail and AI-aware rate limiting prevent unexpected bandwidth costs. Prompt v2.0 ensures consistent, on-brand representation across all AI platforms.

2026-06-11
Performance & Reliability
Tools pages — edge cache for AI-bot traffic, auth-aware unlock, forensic page fix

The Company Intelligence and Forensic Analysis tool pages are now edge-cacheable while staying correct for Pro subscribers and logged-in users. Anonymous visitors (including AI crawlers like Bytespider, Amazonbot, YandexBot, PetalBot, bingbot) now get cached HTML from Cloudflare on repeat visits instead of triggering a full server render on every hit. The Company Intelligence company-profile page is now a static parent that upgrades per-user Pro / purchase state on the client. A latent bug in the Forensic Analysis supplier/department pages — where the cached anonymous variant could be served to Pro subscribers for 6 hours — is now fixed via a Vary directive on the auth-aware sub-routes.

Key Changes

  • The 7 public-safe Company Intelligence routes (landing, search, leaderboard hub, leaderboard [province], live-feed, live-feed embed, awards weekly, awards monthly) now set Cache-Control: public, max-age=120, s-maxage=7200, stale-while-revalidate=86400. Cloudflare can cache these for anonymous visitors and serve repeat hits from the edge instead of round-tripping to the server. The auth-aware [slug] sub-route is intentionally excluded.
  • The Company Intelligence [slug] page is now a static parent that defers per-user Pro / purchase / claim state to a client component. The page no longer calls auth() server-side, so the rendered HTML can be served from cache; per-user state is fetched client-side from /api/v1/report-access/[slug] (5-minute Redis cache per user) and upgrades the page within ~100ms of hydration. Pro subscribers see the full unlocked report (50 award history rows, competitor analysis, full subcontractor network, bidding activity, full forensic details); anonymous and free visitors see the locked default. The paywall / "Claim this profile" / "Track this company" widgets and the Purchase vs Download report button continue to work as before.
  • The Forensic Analysis supplier and department pages now set Vary: Cookie, Authorization in addition to the existing Cache-Control. Cloudflare splits the cache by cookie and Authorization header, so anonymous AI bot and human traffic gets the cached free variant (most of the traffic) while Pro subscribers get a per-user cache entry (or fall through to the server for a fresh SSR). Previously, a Pro subscriber could be served the anonymous (locked) HTML for the full 6-hour TTL.
  • The 11 auth-aware child components in the Company Intelligence and Forensic Analysis trees (Award History, Subcontractor Network, Competitor Analysis, Bidding Activity, Forensic Risk Banner, Director Network Panel, Compliance Score Widget, CIPC Sidebar Card, Forensic Flags Teaser, Forensic Director Panel, Recent Awards Teaser, AI Narrative Panel, Forensic Summary Teaser, Threshold Cluster Table, Director Overlaps Teaser, Address Clusters Teaser, New Entrants Teaser, Supplier Concentration Chart) now read per-user state from a shared access context. The page renders the locked-by-default SSR HTML; the children upgrade after hydration. Anonymous crawlers and SEO crawlers always see the same locked HTML, which is the correct paywall behaviour.

Platform Impact

Repeat anonymous visits (the vast majority of traffic on these routes) are now served from Cloudflare's edge with no server round-trip. The 5-15 GB/day of HTML that AI bots were pulling from these two route trees drops to a small fraction. Auth state stays correct: Pro subscribers see the full report; free visitors see the locked default. No user-visible behaviour change beyond faster page loads and a brief (<100ms) upgrade-from-locked-to-unlocked transition for Pro users on their first visit after this deploy.

2026-06-11
Performance & Reliability
Platform performance: faster tender pages, smarter AI cadence, edge-cached SEO content

Faster page loads and lower platform costs without changing what users see. The tender detail page is now edge-cacheable (it was being rebuilt from the database on every visit). Blog, legislation, province, category, widget, and forensic-analysis pages can now be served by Cloudflare from the edge instead of round-tripping to the main server. Open-Graph preview images for shares are now cached at the edge for 24 hours. The three AI rewrite jobs that ran every 15-20 minutes have been merged into a single job that runs every two hours (NFR-006 cadence) — new tenders get an AI summary within 2 hours, down from every 20 minutes, with a 95% reduction in AI cron traffic and process wake-ups.

Key Changes

  • The user-facing tender detail page at /tenders/[id] is now ISR-cached (1-hour TTL with on-disk Next.js cache + 7-day Redis) instead of running a 6-relation Prisma join + 850-line React render on every request. The SEO twin at /sa-tenders/tender/[slug] was already cached; the user-facing page now matches. Auth-gated children (Track, Bookmark, Pay Now, Apply, Claim) still render correctly as dynamic children of a static parent.
  • The middleware now injects path-scoped Cache-Control headers for known-public ISR routes (/blog/, /legislation/, /sa-tenders/provinces, /sa-tenders/categories, /glossary/, /industries/, /api/widgets/, /tools/forensic-analysis/). Cloudflare can now cache these at the edge — no EC2 hit, no bytes-egress for repeat visitors.
  • 11 of 14 Open-Graph image routes now set Cache-Control: public, max-age=86400, stale-while-revalidate=3600. Social-media share previews no longer re-render the PNG on every crawler visit. The 3 routes that already had this header (general, restricted-suppliers, sections) are unchanged.
  • The 3 separate AI cron jobs (ai-summary-generator every 20 min, enhancement-queue-worker every 15 min, analysis-backfill every 15 min = 264 cron HTTP hits/day, ~95% cold-start overhead) have been collapsed into one job at /api/cron/2h-ai-pipeline running every 2 hours (12 runs/day, ~95% overhead reduction). The merged pipeline runs the same 3 services in sequence: backfill scan → enhancement queue drain → summary batch. The 3 original cron routes remain in the codebase, callable for manual trigger, but no longer run on a schedule. New-tender AI summary visibility delay: up to 20 min → up to 2 h.

Platform Impact

The tender detail page now serves from cache (typical P95 <50 ms vs ~1.5 s SSR). Repeat visitors to /blog/, /legislation/, /sa-tenders/*, and forensic-analysis pages are served by Cloudflare without touching the EC2. EC2 runtime, EC2 data transfer, and AI provider setup overhead all drop materially — the same architecture that was driving the cost, just with the obvious edge cache and cron hot loops closed. No user-visible behavior change for cached pages; auth-gated actions and per-user state continue to work as before.

2026-06-10
Billing & Access
Claim-driven dashboard activation & public profile enrichment

After paying the R1,500 Company Profile Claim fee (PAYSTACK_PRODUCT_COMPANY_CLAIM → PRD_COMPANY_CLAIM), the user's internal Company is created (or reused) and their trial is re-activated if expired. On admin approval, the public SourceOrganization is linked to the internal Company. The dashboard editor at /dashboard/manage-company-profile/[slug] now lets the owner edit their internal Company fields, and the public profile renders an Additional information from the company owner section from those fields. No new charge; the trial re-activation is free and expires into the existing free tier.

Key Changes

  • New additive column SourceOrganization.claimedByCompanyId + back-relation to Company (prisma/tender-domain.prisma) + 1 back-relation on Company (prisma/user-domain.prisma). 1 new @@index.
  • New ClaimActivationService with three helpers: ensureClaimCompany, reactivateClaimSubscription (5-case R2.1.a-e decision tree), and linkSourceOrganizationToCompany (src/lib/services/claim-activation.service.ts).
  • Paystack charge.success webhook on a claim reference now creates the internal Company and re-activates the trial if expired (paystack.service.ts handleChargeSuccess claim branch).
  • Admin PATCH on /api/company/claim/[id] now sets SourceOrganization.claimedByCompanyId when the claim is APPROVED, wrapped in a prisma.$transaction with the existing status update.
  • Public page at /tools/company-intelligence/[slug] renders a new ClaimOwnerEnrichmentSection when the linked Company has populated fields. Renders nothing otherwise. JSON-LD Organization schema gains additive description + contactPoint if the link is set.
  • Dashboard editor at /dashboard/manage-company-profile/[slug] replaces the previous stub with a real form for Company fields (name, registration, tax, B-BBEE, industry, provinces, capabilities) + CompanyProfile.profileText. PATCH route at /api/manage-company-profile/[slug] (NEW).
  • New claim-owner welcome email template (src/lib/email-templates/claim-owner-welcome.ts) + additive export to email-templates/index.ts. Sent on charge.success when the claim is processed.

Platform Impact

A user who pays the R1,500 claim fee now gets immediate dashboard access even if their trial expired, can edit their internal Company profile via a real editor, and the public profile shows their data as Additional information from the company owner. No new charge; the trial re-activation is free and expires into the existing free tier. PayPal path still pending (TODO paypal-claim-payment-hardening).

2026-06-10
Billing & Access
Fixed Paystack company claim webhook (phantom subscription + stuck claims)

When a user paid the R1,500 Company Profile Claim fee via Paystack, the resulting charge.success webhook was being routed to the subscription-activation path because no AddonSubscription row existed for claim payments. The webhook would then create a phantom R250 starter subscription with empty userId/companyId, and the claim itself was left in PENDING_PROOF until the user manually returned to the success page. The webhook now correctly recognises claim payments and marks the claim as PAID + PENDING_REVIEW.

Key Changes

  • Added a CompanyClaim branch to the Paystack charge.success webhook handler. The handler now looks up the claim by its stored Paystack reference, and if the payment is PENDING, transitions it to PAID + PENDING_REVIEW and records the Paystack transaction id and amount.
  • If the claim is already PAID (e.g. the client-side capture beat the webhook), the webhook is now a no-op and logs at info level instead of falling through to the subscription-activation path.
  • No schema change. Uses existing paymentStatus, status, paymentTransactionId, and amountPaid fields on CompanyClaim.

Platform Impact

Claim payments are now reconciled reliably by the webhook. A user who closes the browser before returning to the success page no longer ends up with a phantom subscription row and a claim stuck in PENDING — the webhook closes the loop. Note: a duplicate of this same bug still exists on the PayPal path and is tracked separately in TODO/INDEX.MD (paypal-claim-payment-hardening).

2026-06-10
Billing & Access
Fixed Paystack subscription checkout (Invalid Amount Sent on /pricing)

Paystack checkout on the /pricing page was rejecting new Starter, Professional, and Enterprise subscriptions with a "Paystack API error: Invalid Amount Sent" response. The plan amount is now correctly included with every new-subscription request to Paystack, so users can complete checkout end-to-end.

Key Changes

  • Fixed the new-subscription Paystack request: the plan amount in ZAR (converted to kobo) is now sent alongside the plan code, as required by the Paystack Initialize Transaction API.
  • All three paid plans (Starter, Professional, Enterprise) now load the Paystack Inline checkout overlay as expected when a signed-in user clicks Subscribe on /pricing.
  • No price changes — the displayed R250 / R800 / R2,000 monthly prices remain the source of truth and are sent to Paystack in the request.

Platform Impact

Users can now successfully start a Paystack subscription from the pricing page. Previously, every click on a paid Subscribe button failed with a server-side error and the user was redirected to /payment/failed.

2026-06-09
Billing & Access
Paystack payment integration — new subscriptions and purchases use Paystack

Replaced PayPal as the primary payment processor for all new transactions. New subscriptions, one-time product purchases, and claim payments now go through Paystack with native ZAR support. Existing PayPal subscriptions continue their lifecycle unchanged on PayPal.

Key Changes

  • New Paystack service layer: types, Axios client, plan config, subscription lifecycle service (paystack.service.ts), one-time product purchase service (paystack-package.service.ts), async webhook worker (paystack-webhook-worker.ts), and provider routing service (payment-router.service.ts).
  • Prisma schema: added paystackSubscriptionId (String? @unique), paystackPlanCode, paystackEmailToken to Subscription model; new PaystackWebhookEvent model following the accept-first pattern.
  • API routes: POST /api/webhooks/paystack (accept-first webhook handler), /api/payments/paystack/* (create-subscription, cancel-subscription, verify-transaction, purchase-product), /api/company/claim/[claimId]/paystack-pay and /api/company/claim/[claimId]/paystack-capture.
  • Payment callback: /payment/paystack/callback page verifies transactions and redirects to shared /payment/success on success.
  • Middleware: added /api/webhooks/paystack to PUBLIC_API_ROUTES and Paystack domains to CSP directives (frame-src, script-src, form-action).
  • Pricing updated: starter R499→R600, professional R1,299→R1,200, enterprise R3,999→R2,000. Addon prices updated to match Paystack configured prices. FAQ updated: "PayPal" → "Paystack".
  • Provider routing: getPaymentProvider() routes existing PayPal subscribers to PayPal, all new users to Paystack. Subscription checkout UI calls provider check before selecting the payment flow.
  • Server actions (payment.ts, payment-capture.ts) updated with Paystack path after provider routing check.
  • Env validation: PAYSTACK_SECRET_KEY, PAYSTACK_PUBLIC_KEY, and plan code env vars added to schema and isFeatureAvailable().
  • PayPal service (Tier 2 FROZEN) untouched — continues serving existing subscribers. Tier 1 LOCKED modules (payment.ts, middleware.ts) updated with additive-only changes per approved Impact Assessment.

Platform Impact

All new payments are now processed through Paystack in native ZAR, eliminating PayPal USD conversion costs. Existing PayPal subscribers are unaffected. Pricing reduced across most plans and addons.

2026-06-09
SEO & AI Visibility
Tender Radar province index pages, nav integration, and sitemap

Added province-level index pages at /tender-radar/[category]/[province]/ listing all company profiles for that category and province. JSON-LD restructured with province as Organization, companies as LocalBusiness nodes, plus FAQPage and Note schemas. Tender Radar linked from nav dropdown, shortcuts bar, footer, SA Tenders hub page, and SA Tenders listing page. Added /tender-radar to sitemap.xml at priority 0.9.

Key Changes

  • src/app/tender-radar/[category]/[province]/page.tsx: new province index page with ISR (revalidate: 3600), generateStaticParams for 81 combos, JSON-LD with Organization=Province, OfferCatalog of LocalBusiness companies, FAQPage (4 programmatic Q&A per category+province), Note schema, and BreadcrumbList.
  • src/components/layout/navigation-dropdown-menu.tsx: added Tender Radar as 2nd item in TOOLS_NAV (Target icon).
  • src/components/layout/nav-shortcuts.tsx: added Tender Radar to moreItems dropdown (Radar icon).
  • src/components/layout/footer.tsx: added Tender Radar to Tender Tools section with NEW badge.
  • src/app/sa-tenders/page.tsx: added 4th quick-nav card for Tender Radar to the hub page (lg:grid-cols-4).
  • src/app/sa-tenders/tenders/page.tsx: added "Not Sure Which Tenders Fit Your Business?" CTA card with Try Tender Radar button.
  • src/app/sitemap.ts: added /tender-radar with hourly changeFrequency and 0.9 priority.

Platform Impact

Tender Radar is now discoverable from nav, footer, SA Tenders pages, and search engines. Province index pages surface in Google with proper Organization+LocalBusiness+FAQ structured data, enabling rich results for each category+province combination.

2026-06-07
SEO & AI Visibility
Features, how-it-works, changelog, and intelligence pages now emit canonical JSON-LD via StructuredData

Phase 6 of the SEO Organization & Listing Schema Enhancements completes the remaining page migrations. All 7 feature sub-pages (AI Tender Matching, Tender Document Analysis, Project Implementation Plan, Tender Alerts, Application Assistance, Compliance Gap Analysis, Tender Readiness Score) and the main Features index page are migrated off raw dangerouslySetInnerHTML script blocks onto the canonical XSS-safe StructuredData component. The how-it-works page gains the identity bundle and a HowTo schema describing the 4-step Win Tenders flow. The changelog page gains the identity bundle and a WebPage schema with dateModified set to the most recent changelog entry date. The intelligence feed page gains the identity bundle, a Dataset describing the intelligence corpus (Active Items, Active Sources, Total Sources Monitored), and an FAQPage answering common questions about the feed. All pages now use buildTenderPageIdentityBundle for correct platform Organization attribution in Google knowledge graph.

Key Changes

  • src/app/features/page.tsx (T-050): added identity bundle via buildTenderPageIdentityBundle. The page was the only features page with no JSON-LD at all — it now correctly carries the platform Organization by @id in the knowledge graph.
  • src/app/features/{ai-tender-matching,tender-document-analysis,project-implementation-plan,tender-alerts,application-assistance,compliance-gap-analysis,tender-readiness-score}/page.tsx (T-050): all 7 feature sub-pages had raw <script type="application/ld+json"> blocks for OrganizationSchema + ServiceSchema (and WebSiteSchema on ai-tender-matching) that are now migrated to <StructuredData>. Each page imports StructuredData and buildTenderPageIdentityBundle, builds the identity bundle, and emits <StructuredData data={[...identityBundle, organizationSchema, serviceSchema, ...(faqSchema ? [faqSchema] : [])]} />. The ai-tender-matching page also migrates WebSiteSchema and gains an FAQPage via generateFAQSchema.
  • src/app/how-it-works/page.tsx (T-051): added identity bundle and a HowTo schema via generateHowToSchema. The HowTo describes the 4-step Win Tenders flow: (1) Create your company profile, (2) Get matched to relevant tenders, (3) Apply with confidence, (4) Track your success and improve. Each step carries name + text. The howToSchema is emitted alongside the identity bundle in the StructuredData data array.
  • src/app/changelog/page.tsx (T-052): added identity bundle and a WebPage schema with dateModified set to the most recent changelog entry date (sorted from changelogEntries). The WebPage @id is the changelog URL + #webpage, url is the canonical changelog URL, and dateModified reflects the most recent entry in the data file.
  • src/app/intelligence/page.tsx (T-054): added identity bundle, a Dataset via generateDatasetSchema (variableMeasured: Active Intelligence Items, Active Data Sources, Total Sources Monitored; temporalCoverage: P72H, spatialCoverage: South Africa), and an FAQPage via generateFAQSchema (3 questions covering what the feed is, how often it updates, and what categories are monitored). All schemas are emitted via <StructuredData> at the top of the page.

Platform Impact

All feature pages are now free of raw dangerouslySetInnerHTML script blocks and correctly attributed to the platform Organization. The how-it-works HowTo enables Google to surface the 4-step process in rich results. The changelog WebPage with dateModified helps search engines understand content freshness. The intelligence Dataset enables AI assistants to cite real procurement signal data (NFR-009 Citation Hook). No AggregateRating, no frozen module touched.

2026-06-07
SEO & AI Visibility
Blog and legislation pages now carry identity bundle, RelatedTendersList, Legislation, GovernmentService, and GovernmentOrganization schemas

Phase 5 of the SEO Organization & Listing Schema Enhancements extends identity bundle coverage and emits domain-specific schemas on blog and legislation pages. Blog post pages gain the identity bundle and a RelatedTendersList linking to relevant tenders via generateRelatedTendersSchema. Blog category pages gain the identity bundle plus Legislation schemas for each law relevant to that category (via mapCategoryToLegislation). Blog author pages gain the identity bundle. The legislation index page gains a CollectionPage with hasPart listing all SA_LEGISLATION acts and the identity bundle. Each legislation sector page gains a GovernmentService + ItemList + identity bundle. Each legislation act page gains the identity bundle and a GovernmentOrganization for the regulator (National Treasury for PFMA/MFMA/PPPFA, B-BBEE Commission for BBBEE Act, CIDB for CIDB Act, etc.).

Key Changes

  • src/app/blog/[slug]/page.tsx (T-041): the page now imports buildTenderPageIdentityBundle, generateRelatedTendersSchema, and getRelatedTendersForBlog. After the baseUrl and canonicalUrl are defined, the page calls buildTenderPageIdentityBundle with the blog post URL and breadcrumbs (Blog, category, title) and calls generateRelatedTendersSchema with up to 6 related tenders fetched via getRelatedTendersForBlog. Both are spread into allSchemas before the existing structuredData and breadcrumbStructuredData, so the entity graph connects the blog post to the platform Organization and to related tenders.
  • src/app/blog/category/[slug]/page.tsx (T-042): added identity bundle from buildTenderPageIdentityBundle and legislation schemas from generateLegislationSchema for each law returned by mapCategoryToLegislation(slug). The StructuredData data prop now spreads ...identityBundle before collectionSchema and breadcrumbSchema, and appends ...legislationSchemas so the category page mentions the applicable procurement laws.
  • src/app/blog/author/[slug]/page.tsx (T-043): added identity bundle from buildTenderPageIdentityBundle. The StructuredData data prop now spreads ...identityBundle before profileSchema and articleListSchema.
  • src/app/legislation/page.tsx (T-045): added identity bundle and a CollectionPage with hasPart listing all SA_LEGISLATION acts. The @graph now opens with ...identityBundle, then the CollectionPage (with @id, url, name, description, inLanguage, isPartOf, hasPart), then breadcrumbSchema, then all legislation schemas.
  • src/app/legislation/sectors/[slug]/page.tsx (T-046): added identity bundle + GovernmentService (describing the sector compliance service, provider by @id) + ItemList via generatePaginatedListingSchema listing the related acts. The existing breadcrumbSchema is preserved. No raw dangerouslySetInnerHTML; all via StructuredData.
  • src/app/legislation/acts/[slug]/page.tsx (T-047): added identity bundle and a GovernmentOrganization for the law's regulator (REGULATOR_FOR_LAW map: PFMA/MFMA/PPPFA -> National Treasury, BBBEE Act -> B-BBEE Commission, CIDB Act -> CIDB, PSA -> DPSA, POPIA -> Information Regulator, constitution-s217 -> Government of South Africa). The StructuredData @graph opens with ...identityBundle, then breadcrumbSchema, then the Legislation schema, then the regulator GovernmentOrganization.

Platform Impact

Blog posts are now connected to the platform Organization and to related tenders in the entity graph, enabling Google to understand "this article is about procurement law X and mentions tender Y". Blog categories and legislation pages expose the full hierarchy of procurement law — sector -> act -> regulator — so AI assistants can answer "what laws govern construction tenders in South Africa" or "who regulates CIDB". No AggregateRating, no raw script blocks, no frozen module touched.

2026-06-07
Company Intelligence
Claim workflow: local-disk storage, 10MB cap, Pay Now button, working Resume Claim URL

Four fixes to the company profile claim flow: (1) Proof-of-ownership documents are now saved to the main app's local disk at a deterministic path keyed by claim ID, regardless of R2 or Worker storage environment variables. (2) The 10MB per-file cap is enforced server-side on the new multipart upload route, with clear 413 errors. (3) The PayPal CTA in the claim payment step now reads "Pay Now" with the price shown as helper text. (4) The "Resume Claim" button in the Claim Started email now links to the public company page with the modal auto-opening at the exact step the claimant left at, using a new claimId deep-link parameter. The "Claim Under Review" email CTA was also corrected (it was previously pointing to the admin-only /admin/company-claims route).

Key Changes

  • New POST /api/company/claim/[claimId]/upload-proof route accepts multipart/form-data, enforces 10MB cap and PDF/PNG/JPEG mime allowlist, writes files to process.cwd()/uploads/company-claims/{claimId}/ with path-traversal protection, and stores both proofDocumentStorageKey and proofDocumentUrl in the CompanyClaim row.
  • New GET /api/company/claim/[claimId]/proof-file route streams the saved file with auth gating (claimant or admin only), inline Content-Disposition, and returns 410 with a claim_proof_file_missing log entry if the file is missing on disk.
  • Additive proofDocumentStorageKey String? column on CompanyClaim (prisma/user-domain.prisma) — no data migration required for existing rows.
  • New claimUploadFile server action in src/app/actions/claim-upload-file.ts; FileUpload component now accepts an optional claimId prop and routes to the claim-specific endpoint when provided.
  • ClaimWorkflow.tsx: Pay Now button relabeled (was "Pay R1,500 with PayPal"), price moved to helper text. Added initialClaimId prop and a separate useEffect that calls resumeFromClaim(id) on mount, skipping the POST /api/company/claim initiate path.
  • ClaimProfileCta.tsx: reads searchParams.claimId and passes it as initialClaimId to ClaimWorkflow, so the ?openClaim=1&claimId=<id> deep link auto-resumes the correct step.
  • Email templates: claim-submitted.ts and claim-pending-review.ts now require companySlug in their data shapes. The Resume Claim CTA URL changed from /dashboard/claims (404) to /tools/company-intelligence/{slug}?openClaim=1&claimId={id}. The Claim Under Review CTA URL changed from /admin/company-claims (admin-only) to the same public-page deep link.
  • admin-claim-notification.service.ts: NotifyClaimantClaimSubmittedArgs and NotifyClaimantClaimPendingReviewArgs gained a required companySlug field. Both call sites in api/company/claim/route.ts and api/company/claim/[claimId]/capture/route.ts updated to pass companySlug from the Prisma claim record.
  • .env.example: added CLAIM_PROOF_STORAGE_PATH= comment block documenting the production persistent-volume requirement.
  • .gitignore: added uploads/ to prevent claim files from being committed.

Platform Impact

The broken resume URL is fixed — claimants who close the modal mid-flow can now click the email link and land back at the exact step they left. The "Claim Under Review" email no longer points admins-only routes at claimants. Claim files are now stored on the main app's local disk deterministically (per CLAIM_PROOF_STORAGE_PATH) instead of silently leaking to R2/Worker. The 10MB cap is enforced server-side as a defense in depth against client-side bypass. No Tier 1 frozen modules modified. Email Templates and Notification Service (both Tier 2) received additive signature changes — all call sites updated in the same PR.

2026-06-07
SEO & AI Visibility
Publishers Hub migrated to canonical XSS-safe StructuredData with identity bundle

Phase 4 of the SEO Organization & Listing Schema Enhancements completes the publisher migration. The Publishers Hub index (/publishers), all 5 sector pages (/publishers/[sector]), and the developers page (/publishers/developers) now use the canonical <StructuredData> component (XSS-safe) instead of raw <script type="application/ld+json"> blocks. The publishers/index and sector pages also gain the identity bundle (Organization by @id + WebSite + BreadcrumbList) so the entity graph correctly attributes these pages to the platform. The developers page now uses the canonical TechArticle schema via StructuredData instead of a hand-rolled dangerouslySetInnerHTML block. The dashboard page (/publishers/dashboard) is excluded — it is a client-only authenticated analytics view where JSON-LD would be misleading.

Key Changes

  • src/app/publishers/page.tsx (T-027): added imports for buildTenderPageIdentityBundle and StructuredData. The page now emits the identity bundle alongside the existing <JsonLdSchemas /> component that was migrated in Phase 1. The identity bundle carries the platform Organization by @id, so the /publishers page is correctly attributed to the Tenders SA entity in Google.
  • src/app/publishers/[sector]/page.tsx (T-026): replaced the file-local SectorJsonLd component (3 raw <script type="application/ld+json"> blocks — BreadcrumbList, WebPage, FAQPage) with a new SectorStructuredData component that uses <StructuredData> with canonical generators. The BreadcrumbList is now built via buildTenderBreadcrumb (from @/lib/seo/tender-page-schemas), the FAQPage via generateFAQSchema (from @/lib/structured-data), and the identity bundle via buildTenderPageIdentityBundle. All 5 sector pages (construction, energy, healthcare, it, transport) are covered by the single dynamic route.
  • src/app/publishers/developers/page.tsx (T-026): replaced the raw dangerouslySetInnerHTML TechArticle script block with a <StructuredData data={[...identityBundle, techArticleSchema]} /> call. The techArticleSchema uses the canonical provider reference and inLanguage: en-ZA. The identity bundle is built with buildTenderPageIdentityBundle with extraBreadcrumbs (Publishers Hub to Developer Documentation).

Platform Impact

The Publishers Hub and all its sub-pages are now fully migrated to the canonical XSS-safe StructuredData path. Google will no longer encounter any raw JSON-LD script blocks on these pages. The identity bundle on the index and sector pages means the platform Organization is correctly linked to these pages in the knowledge graph. No frozen module touched, no Prisma change, no new dependency introduced.

2026-06-07
SEO & AI Visibility
Listing pages now expose AdministrativeArea, GovernmentOrganization, Service, and Dataset in the entity graph

Phase 3 of the SEO Organization & Listing Schema Enhancements rollout fills the listing-page schema gaps identified in the Phase 1 audit. All 5 listing families — province, organization, organization-type, tender+category, and awards — now carry a per-page subject entity in the @graph. Province notices + cancellations pages gain the AdministrativeArea the index page already had. The publisher-type filter page (/sa-tenders/organizations/type/municipalities, /state-owned-enterprises, /national-departments, /provincial-departments) gains a synthetic GovernmentOrganization with `additionalType` set per URL segment. The 4 category sub-pages (closed / awarded / notices / cancellations) gain a per-page Service describing the category itself plus an ItemList built via the canonical generatePaginatedListingSchema. The 3 awards pages gain a Dataset with variableMeasured describing the leaderboard data. No raw <script type="application/ld+json"> blocks, no AggregateRating fabrication, no frozen module touched.

Key Changes

  • src/app/sa-tenders/provinces/[province]/notices/page.tsx + src/app/sa-tenders/provinces/[province]/cancellations/page.tsx: each page now imports generateAdministrativeAreaSchema from @/lib/structured-data, hoists baseUrl into a const (replacing the inline env fallbacks in the existing JSON-LD), and appends the generated AdministrativeArea to the data array passed to <StructuredData>. The generated schema carries url, geo, description, and containedInPlace: { @type: Country, name: South Africa, identifier: ZA } — the same shape the provinces [province]/page.tsx index page already emits.
  • src/app/sa-tenders/organizations/type/[type]/page.tsx (T-022): a new exported TYPE_ADDITIONAL_TYPE constant maps the 4 publisher-type URL slugs to the formal additionalType labels (Municipality / StateOwnedEnterprise / GovernmentDepartment). A new typeOrganizationSchema (built inline, not via generateGovernmentOrganizationSchema) emits a synthetic GovernmentOrganization describing the type category. The synthetic entity has @id = `${pageUrl}#organization-type` (unique per page, so it does not collide with the canonical platform Organization @id), name = typeData.name, additionalType = TYPE_ADDITIONAL_TYPE[type] (with a fallback to typeData.name if the slug is unknown), description, areaServed: { @type: Country, name: South Africa }, inLanguage: en-ZA, isPartOf: { @id: https://www.tenders-sa.org/#website }, and knowsAbout populated from the visible org names on the page. The page is the canonical entry point for the "Municipalities" and "SOE" listings the user prompt asks us to expose to the knowledge graph (Q4 sign-off, INTEGRATION_EVAL.md §0).
  • src/app/sa-tenders/categories/[category]/{closed,awarded,notices,cancellations}/page.tsx (T-023): each page now imports generatePaginatedListingSchema from @/lib/structured-data and CANONICAL_ORG_ID from @/lib/seo/tender-page-schemas. The previous "buildTenderPageIdentityBundle-as-data" pattern is wrapped into a jsonLd const so the @graph can carry the identity bundle + a new categoryServiceSchema (Service describing the category itself, provider by @id) + a listingSchema (ItemList of the visible tenders). The notices + cancellations pages were the only ones that still used the inline-as-data pattern — the refactor preserves the same data and emission path. The closed + awarded pages were already using a jsonLd const and just gained the new entities. No FAQPage is emitted on any of these 4 sub-pages — the spec is explicit ("do NOT invent") and the only FAQ source (categorySEO.faqSchema) is on the category index.
  • src/app/sa-tenders/awards/{provinces,categories,organizations}/page.tsx (T-024): each page now imports generateDatasetSchema from @/lib/structured-data. The provinces and categories pages append a Dataset to the existing @graph (the leadersboard pattern from .agent/skills/leaderboard-engineering/SKILL.md and the Citation Hook rule, NFR-009). The organizations page refactors from "identity-bundle-as-data" to a jsonLd const and appends a single Dataset (the page has 4 sub-aggregations — SOEs, Municipalities, National Departments, Provincial Departments — so 4 variableMeasured entries). Each Dataset carries temporalCoverage: P12M, spatialCoverage: { @type: Country, name: South Africa }, and 3-4 variableMeasured entries built from the actual page-computed stats (Total Awards, New This Week, Provinces/Categories/OrgTypes with Awards). No fabricated "Total Awarded Value" entry — the data layer does not yet compute that aggregation; adding it is a Phase 4+ data-layer concern, not a schema-layer one.

Platform Impact

The 5 listing families now carry a per-page subject entity in the @graph, so Google can answer "what is this listing about" from a typed entity. The publisher-type filter page is the discovery surface for the "Municipalities" and "SOE" listings the user prompt asked for — those URLs now have a typed GovernmentOrganization in the knowledge graph instead of just a CollectionPage. The 4 category sub-pages (closed / awarded / notices / cancellations) now have a Service describing the category itself (not just a per-tender list) and an ItemList of the visible tenders, which is the entity-graph pattern Google uses to surface category-indexed sub-views. The 3 awards pages now expose a Dataset that AI assistants can cite when answering "how many SA government awards are there" or "which provinces have the most awards" — Citation Hook rule (NFR-009). No fabricated star-rating schema, no real third-party review source required, no frozen module touched.

2026-06-07
SEO & AI Visibility
Home page + Pricing page now emit typed JSON-LD (FAQPage, SoftwareApplication, Dataset, Service, BreadcrumbList)

Phase 2 of the SEO & listing rollout composes the home page and pricing page JSON-LD through the canonical XSS-safe <StructuredData> wrapper. The home page now emits 4 page-specific entities (FAQPage from HOMEPAGE_FAQS, a Tenders SA platform SoftwareApplication with provider by @id, a Dataset describing the 4 visible stats, and the Procurement Assistant SoftwareApplication from Phase 1). The pricing page emits 4 × Service (one per PRICING_PLANS entry) plus a BreadcrumbList and a FAQPage from PRICING_FAQS. The hand-rolled <script type="application/ld+json"> block on the home page (lines 342-353) is removed; the local unlinked generateWebSiteSchema is dropped to avoid a duplicate WebSite entity next to the layout's @id-tagged buildTendersSaWebSite(). Testimonials on the home page (lines 846-924) stay visual-only — no AggregateRating or Review is emitted, no fabricated star-rating schema, no real third-party review source required.

Key Changes

  • src/lib/structured-data.ts: DatasetSchemaInput interface is now exported (was internal) so the home page can pass a typed variableMeasured payload to generateDatasetSchema. The function signature and return shape are unchanged, so existing internal callers are unaffected.
  • src/app/page.tsx: imports gain generateDatasetSchema, generateProcurementAssistantSchema, StructuredData (from @/components/seo/structured-data), and CANONICAL_ORG_ID (from @/lib/seo/tender-page-schemas). The local generateWebSiteSchema import is removed (the layout already emits the @id-tagged WebSite).
  • src/app/page.tsx: a new platformSchema constant is declared as a SoftwareApplication (Tenders SA, applicationCategory: BusinessApplication, applicationSubCategory: GovernmentTenderDiscoveryPlatform, areaServed: South Africa, offers: FreeTrial ZAR, provider: { @id: CANONICAL_ORG_ID }). It ties the home page to the canonical organization entity, not to a re-declared Tenders SA.
  • src/app/page.tsx: a new datasetSchema constant is built via generateDatasetSchema with name "Tenders SA Live Tender & Awards Dataset", a description of the data product, a 12-keyword keywords string, four variableMeasured entries mapping 1:1 to the 4 stat cards rendered at lines 626-642 (Active Tenders, Verified Awards, Added This Week, Awarded Companies), spatialCoverage: { @type: Country, name: South Africa }, and an open-ended temporalCoverage. The Dataset describes the source data, not invented entity claims.
  • src/app/page.tsx: a new assistantSchema constant is built via the Phase 1 generateProcurementAssistantSchema(BASE_URL) helper. The Procurement Assistant is the only SoftwareApplication whose @id is the home page itself (no @id on the entity, but the provider links by @id to the platform Organization).
  • src/app/page.tsx: the raw <script type="application/ld+json"> block at lines 342-353 (which had been emitting websiteSchema + faqSchema through an un-escaped dangerouslySetInnerHTML) is removed and replaced with a single <StructuredData data={[faqSchema, platformSchema, datasetSchema, assistantSchema].filter(Boolean)} /> call. The script body is now passed through the same safeJsonLd XSS escape used by every other JSON-LD emission on the platform (closes the same <, >, &, U+0027 vector that was fixed on Publishers Hub in Phase 1 and on tool pages / tender routes in prior SEO passes).
  • src/app/page.tsx: the 3 testimonials at lines 846-924 (Thabo Mthembu / Sarah Naidoo / Pieter van der Merwe) remain visual-only. No AggregateRating and no Review entity is emitted — no fabricated star-rating schema, no real third-party review source, no manual-action risk.
  • src/app/pricing/page.tsx: imports gain generateBreadcrumbSchema, generateFAQSchema, generatePricingPlanSchema, type BreadcrumbItem, and StructuredData. The existing PRICING_PLANS / PRICING_ADDONS / PRICING_FAQS import is unchanged.
  • src/app/pricing/page.tsx: a new breadcrumbSchema is built (Home → Pricing) via generateBreadcrumbSchema. A new pricingPlanSchemas array is built by mapping plansForSchema (one PricingPlanSchemaInput per PRICING_PLANS entry) through the Phase 1 generatePricingPlanSchema(plan, baseUrl) helper. The free plan's Service carries the additionalProperty: { name: "BillingModel", value: "Pay-per-use" } emitted by the generator; the Professional plan's Service carries category: "RecommendedSubscription". All 4 plan Services carry provider: { @id: CANONICAL_ORG_ID }. A new pricingFaqSchema is built from PRICING_FAQS via generateFAQSchema.
  • src/app/pricing/page.tsx: the return is wrapped in a fragment and a single <StructuredData data={[...pricingPlanSchemas, breadcrumbSchema, pricingFaqSchema].filter(Boolean)} /> is rendered as a sibling of the existing <Suspense>. The Suspense + PricingClient call sites are unchanged.

Platform Impact

Google now sees a complete entity graph on the home page (canonical Organization + WebSite from the layout; FAQPage, platform SoftwareApplication, Dataset, and Procurement Assistant SoftwareApplication from the page) and on the pricing page (canonical identity from the layout; 4 × Service subscription plan, BreadcrumbList, FAQPage from the page). AI assistants citing "what is Tenders SA", "how much does Tenders SA cost", or "is there a free trial" can answer from typed JSON-LD (Service offers with ZAR UnitPriceSpecification; FAQPage with verbatim Q&A) rather than scraping the visible HTML. The Dataset describes real platform data, not invented entity claims. The XSS escape on the home page script block closes the same vector that was fixed on Publishers Hub in Phase 1. No fabricated star-rating schema, no real third-party review source required, no frozen module touched (no Prisma change, no email provider change, no auth change).

Related://pricing
2026-06-07
SEO & AI Visibility
Canonical Tenders SA Organization identity is now complete (Facebook, LinkedIn Group, X handle, WhatsApp contact, ZA address)

The platform Organization entity emitted on every page through the global layout and the per-page identity bundle now carries the full social/contact/address set the brand owns. The 2-entry TENDER_SA_SOCIAL array is replaced with a 3-entry canonical sameAs (Facebook, LinkedIn Group, X handle), a customer-support ContactPoint carries the WhatsApp cell + wa.me URL + support email, and the address is country-only (addressCountry: ZA) per the user's "just use South Africa" decision. Phase 1 also adds three new schema generators (generatePricingPlanSchema, generateProcurementAssistantSchema, generateRelatedTendersSchema) and migrates the Publishers Hub off raw <script type="application/ld+json"> blocks onto the XSS-safe <StructuredData> component.

Key Changes

  • src/lib/structured-data.ts: new exported ContactPointData interface and contactPoint? field on OrganizationData. generateOrganizationSchema now emits a nested contactPoint array (one entry per ContactPointData) when present; the existing 8+ fields are unchanged, so all current callers continue to emit the same payload by default.
  • src/lib/seo/tender-page-schemas.ts: TENDER_SA_SOCIAL is replaced (not appended) with a 3-entry canonical list — https://www.facebook.com/tenderssaorg, https://www.linkedin.com/groups/17396017/ (Tenders SA LinkedIn group, primary social), and https://x.com/blueitserver. The Custom Logic company LinkedIn URL is intentionally excluded per the Q1 sign-off; the brand entity stays "Tenders SA". A new exported TENDER_SA_CONTACT_POINT carries the WhatsApp cell (telephone +27-60-705-6019, url https://wa.me/27607056019, email [email protected], areaServed South Africa, availableLanguage en-ZA).
  • src/lib/seo/tender-page-schemas.ts: buildTendersSaOrganization() now passes the new sameAs, the canonical contactPoint, and a country-only address (addressCountry: ZA only — no street, no city, no region, no postal code) per the Q3 sign-off. The @id (https://www.tenders-sa.org/#organization) is preserved so the Google knowledge graph entity stays unified; the hard-coded "Cape Town / Western Cape" address is removed.
  • src/lib/structured-data.ts: GovernmentOrganizationData gains an additionalType?: string | null field. generateGovernmentOrganizationSchema prefers the new field when present and falls back to the existing organizationType for backward compatibility, so the type-filter page (municipality / soe / national / provincial) can pass an exact label without the underscore-to-space transform.
  • src/lib/structured-data.ts: new generator generatePricingPlanSchema(plan, baseUrl) emits one Service per PRICING_PLANS entry, with a UnitPriceSpecification (unitCode: MON), priceCurrency: ZAR, availability: https://schema.org/InStock, and a pay-per-use additionalProperty on the free plan. provider is the platform Organization by @id.
  • src/lib/structured-data.ts: new generator generateProcurementAssistantSchema(baseUrl) emits a SoftwareApplication with applicationCategory: ProcurementAssistantApplication, applicationSubCategory: GovernmentContractAdvisoryAI, a 10-item featureList matching the visible AI capabilities, and a 12-item knowsAbout enumerating the SA procurement-law corpus (PFMA, PPPFA, B-BBEE Act, CIDB Act, PSA, MFMA, CSD, SARS, OCPO, National Treasury, eTenders Portal, tender documentation/GO-NO-GO). The visible UI label stays "AI Assistant" (Q6 schema-only change).
  • src/lib/structured-data.ts: new helper generateRelatedTendersSchema({ tenders, postUrl, baseUrl, maxItems? }) wraps generatePaginatedListingSchema and sets mainEntityOfPage on the inner ItemList so the blog post is connected to the listed tenders in the entity graph.
  • src/app/publishers/components/json-ld-schemas.tsx: 4 raw <script type="application/ld+json"> blocks replaced with a single <StructuredData data={[...]} /> call. The 4 widget SoftwareApplication schemas stay; their provider now uses the canonical Organization by @id (not a self-link). The file-local generateOrganizationSchema / generateWebPageSchema / generateSoftwareApplicationSchemas / generateFAQPageSchema helpers are removed; the FAQPage uses the canonical generateFAQSchema(FAQS).

Platform Impact

The platform Organization on every page now exposes the same set of social, contact, and geographic signals that the brand actually operates, so Google can build a single knowledge-graph panel for Tenders SA (Facebook page, LinkedIn group, X handle) with a customer-support ContactPoint for direct WhatsApp / voice / email outreach. AI assistants citing "what is Tenders SA" or "how do I contact support" can answer from a typed, XSS-escaped schema rather than scraping the visible HTML. The Publishers Hub migration closes the same <, >, &, U+0027 XSS vector that was fixed on the tool pages and tender routes in prior SEO passes. No fabricated star-rating schema, no real third-party review source required, no frozen module touched (no Prisma change, no email provider change, no auth change).

2026-06-07
Company Intelligence
Claim workflow now resumes after interruption

Users who close the claim modal mid-flow can now return and pick up at the correct step (upload proof, pay, or "awaiting review") instead of getting stuck. Rejected claims retain their full audit history.

Key Changes

  • Claim workflow now resumes from the correct step when a user re-clicks "Claim This Profile" after a PayPal cancel, browser close, or network blip.
  • A new "Resume your claim" CTA appears on the public profile for users with an in-progress claim.
  • Rejected claims are no longer deleted on re-claim — the prior row and its admin review history are preserved.
  • The "Verified Profile" badge now only appears for admin-approved claims, fixing a false positive for pending claims.
  • New users who click "Claim This Profile" while logged out are now correctly returned to the claim modal after registration and email verification.

Platform Impact

Reduces friction in the R1,500 claim flow and prevents paying users from being locked out of the feature.

2026-06-07
Company Intelligence
Admins can now review company claims

A new /admin/company-claims page lets admins find, filter, review, approve, and reject pending claim requests. Claimants receive emails on every state transition. Approved claims unlock a "Manage this profile" dashboard page.

Key Changes

  • New admin page at /admin/company-claims with Pending / Approved / Rejected / All tabs, filters by date / company / status, and pagination.
  • Claim detail modal previews the proof document and includes an admin-notes textarea for approve / reject actions.
  • Claimants now receive a "claim received" email when payment is captured, an "approved" email with a link to the profile editor, or a "rejected" email with the admin's reason.
  • Admins receive a "new claim needs review" email with a deep link to the claim whenever a payment is captured.
  • Approved claims unlock a new "Manage this profile" widget on the public page and a stub editor at /dashboard/manage-company-profile/[slug].

Platform Impact

Completes the claim feature's missing half — admins can now actually review claims, and the user promise of "Pay R1,500, get verified + edit access" is finally deliverable end-to-end.

2026-06-06
SEO & AI Visibility
Company profile pages now expose knowsAbout + sponsor; new <DynamicSchema> wrapper for typed JSON-LD

Company intelligence profile pages used to emit a single Organization JSON-LD block with a hand-rolled <script> and an incorrectly-nested BreadcrumbList. They now emit a typed <DynamicSchema type="companyProfile"> payload that includes the supplier's top awarded categories as Organization.knowsAbout and the supplier's most frequent awarding bodies as Organization.sponsor, derived from live award data. A new <DynamicSchema> component is the canonical wrapper for page-specific JSON-LD — a discriminated union of typed `type` values mapped to schema builders, rendering through the same XSS-safe <StructuredData> used across the platform. The first registered types are companyProfile (used here) and tender (reserved for tender detail migrations).

Key Changes

  • New <DynamicSchema> component at src/components/seo/dynamic-schema.tsx: a typed discriminated-union wrapper. Callers pass type="companyProfile" | "tender" plus a matching data payload; the wrapper routes to a registered builder and renders the resulting schema graph through the canonical <StructuredData> component (XSS-safe, dev-validated). New page-level JSON-LD should be added here rather than hand-rolling <script type="application/ld+json"> blocks.
  • New getSupplierTopCategories(supplierName, limit) helper in src/lib/data-access/tender-history.ts that returns the top categories in which a supplier has received awards, ordered by the pre-computed TenderCategory.tendersCount as a best-effort proxy. Query path: TenderCategory -> TenderCategoryRelation -> Tender -> TenderAward.supplierName.
  • src/app/tools/company-intelligence/[slug]/page.tsx: the page-level getCompanyData() cache now also returns topCategories (the supplier's top 8 awarded categories) and topSponsors (the supplier's top 5 most-frequent awarding bodies, deduped from award history with sourceOrganizationRelation.name preferred over the raw sourceOrganization string).
  • src/app/tools/company-intelligence/[slug]/page.tsx: the hand-rolled <script type="application/ld+json"> Organization block (lines 295-320) — which had a BreadcrumbList incorrectly nested inside the Organization and was emitting raw JSON via dangerouslySetInnerHTML — is replaced with <DynamicSchema type="companyProfile" data={...} />, which now also emits knowsAbout (top categories), sponsor (top awarding bodies), and is XSS-escaped through safeJsonLd.
  • The paywall WebPage schema (lines 322-331) is unchanged — it continues to be emitted as a separate <script> block. The paywall schema is intentionally not part of the DynamicSchema wrapper: it is a one-off per-page marketing signal, not an entity.

Platform Impact

Google now sees real, evidence-backed knowsAbout (top awarded categories) and sponsor (top awarding bodies) on every company profile, which is the entity-graph pattern Google uses to connect suppliers to their industry and their buyers. AI assistants citing "what does this company do" or "who buys from this company" can now answer from a typed <DynamicSchema> graph instead of scraping the visible HTML. The XSS-escape on the Organization JSON-LD closes the same <, >, &, U+0027 vector that was fixed on the tool pages and tender routes in prior SEO passes. The <DynamicSchema> wrapper is the new home for any future page-specific JSON-LD (tender, blog, leaderboard, program pages) — agents and humans should add to its registry rather than hand-rolling new <script> blocks.

2026-06-06
SEO & AI Visibility
Tender detail pages now nest a Tender Enquiries ContactPoint on the awarding organization

Tender detail pages used to emit the awarding organization's contact email and phone as flat properties on the GovernmentOrganization entity. They now nest a ContactPoint inside the org — the entity-graph pattern that connects the awarding body to the specific channel used for bid enquiries — with the phone normalized to E.164 (+27) so Google can resolve the contact across markets and the platform can be cited as a source for procurement-officer contact details by AI assistants.

Key Changes

  • src/lib/structured-data.ts: generateContactPointSchema() now accepts an optional 5th availableLanguage parameter (string or string[]). Old callers are unaffected — the new param is opt-in. The internal call from generateGovernmentOrganizationSchema() continues to work as before with no availableLanguage set.
  • src/app/sa-tenders/tender/[slug]/page.tsx: the organizationSchema for the awarding body now emits a nested contactPoint (contactType: "Tender Enquiries", areaServed: "ZA", availableLanguage: ["en-ZA"]) built via the existing generateContactPointSchema helper. The previously-flat email and telephone properties on the GovernmentOrganization are removed — the contact lives in the ContactPoint where Google expects it.
  • src/app/sa-tenders/tender/[slug]/page.tsx: the awarding body's phone is now normalized through normalizePhoneSA() (E.164) before being passed to the ContactPoint, so 012-345-6789 / 021 555 1234 / +27 11 555 1234 / 27115551234 all surface as +27XXXXXXXXX. Invalid phones fall back to the raw value, so the schema is never dropped because of a formatting issue.
  • The named procurement officer schema (generateContactPersonSchema) is unchanged — it is a Person, not a ContactPoint, and continues to be emitted alongside the ContactPoint for the named officer's direct line.

Platform Impact

Awarding organizations on tender detail pages now expose a proper ContactPoint (Tender Enquiries) in their entity graph, which is the schema.org pattern Google uses to surface direct procurement contact information in search and which AI assistants cite when answering "who do I call about this tender". The +27 E.164 normalization means a phone number entered as 012 345 6789 in our raw data lands in the JSON-LD as +27123456789, the format Google accepts across markets. No new npm dependencies, no Prisma schema changes, no Tier 1 or Tier 2 module modifications. Backwards compatible: the generateContactPointSchema signature change is purely additive.

2026-06-06
SEO & AI Visibility
Identity bundle and structured data applied to all 12 tool landing pages

Every Tool page on Tenders SA — the B-BBEE calculator, CIDB grade calculator, compliance checklist, preparation planner, readiness assessment, AI proposal generator, value estimator, JV calculator suite, forensic analysis, company & director lookup, provincial tender heatmap, company intelligence, and the tools landing — now emits a single canonical Organization + WebSite (SearchAction) + BreadcrumbList JSON-LD identity bundle alongside the page-specific schema (SoftwareApplication, WebPage, CollectionPage, ItemList, FAQ, Dataset). This mirrors the same identity bundle already applied to every tender route, so the platform's Google knowledge graph entity now spans both sections and AI assistants / crawlers can confidently cite the tools. The bare /tools/template-generator URL is now crawlable; only the deep-link /tools/template-generator?tenderId=...&tenderRef=... variants (the per-tender proposal drafts from the active-tender drawer) are noindex/nofollow so they don't pollute the index. The heatmap schema previously carried a fabricated 4.8/120 review rating — that has been removed; the dataset is now described accurately from the live tender counts and total published value.

Key Changes

  • New shared helper buildToolPageBundle() in src/lib/seo/tool-page-schemas.ts that emits the canonical Organization + WebSite (SearchAction) + BreadcrumbList identity triple plus a per-tool SoftwareApplication schema (name, description, URL, applicationCategory, applicationSubCategory, featureList, free or premium offer, inLanguage, areaServed=South Africa, provider/publisher=@id to the platform Organization).
  • Migrated 13 tool page files from hand-rolled <script type="application/ld+json"> blocks to the XSS-safe <StructuredData> component so the JSON-LD payload escapes <, >, &, and \u0027 — closing the same XSS vector that was fixed on the tender routes in the prior SEO pass.
  • Per-page schema additions: landing tools page emits an ItemList of 15 SoftwareApplications (1st–15th position), forensic analysis emits a WebPage with `about` and `isPartOf` linked to the platform WebSite, company intelligence emits a CollectionPage plus an ItemList of province-level leaderboard pages, value estimator and template generator declare premium-tier Offer prices, and the heatmap emits a real Dataset schema with live tender count and total published value.
  • FAQ schema added where visible FAQ content already exists: compliance-checker (3 questions), cidb-calculator (3 questions), company-director-lookup (3 questions), company-intelligence (4 questions), forensic-analysis (4 questions).
  • Template generator metadata now uses generateMetadata({ searchParams }) to emit noindex/nofollow + a noindex googleBot directive only when query parameters are present, and a self-referential canonical URL. The bare /tools/template-generator URL stays indexable; the deep-link /tools/template-generator?tenderId=...&tenderRef=...&tenderTitle=... variants are now blocked from indexing.
  • Removed /tools/template-generator from the protectedPaths list in the robots route so the bare tool URL is crawlable; deep-link query-string variants are excluded at the metadata level only.
  • HeatmapSchema component rewritten to use the new helper and to drop the fabricated aggregateRating (4.8/120) and the hard-coded TendersSA creator name. The Dataset schema now describes the dataset accurately from live stats and references the platform Organization via @id.

Platform Impact

Tools section pages now share the same single mergeable Tenders-SA entity in the Google knowledge graph as the tender section, so brand signals (sitelinks search box, organization panel) propagate across the full site. AI assistants and crawlers get a richer, accurate description of each tool — name, what it does, the data it touches, the price tier, and where in South Africa it applies — making them far more likely to cite the right tool page when a user asks about B-BBEE levels, CIDB grades, tender value, or procurement intelligence. Eliminates a stored-XSS class on every tool page. Removes a fabricated user-rating field that could have triggered a Google structured-data manual action.

2026-06-06
AI Matching & Intelligence
Tender detail pages now show a document analysis progress indicator

Tender detail pages now display a three-step document analysis progress indicator on the support documents panel — Reading the tender document → Compliance review → Bid-ready summary — so users can see at a glance whether each support document is still being read, has been analysed, or has been summarised into bid-ready form. The indicator is fully server-rendered, accessible, and uses the same status as the analysis panel below it.

Key Changes

  • New server component src/components/tender/DocumentAnalysisStageIndicator.tsx maps the existing analysis status (aiExtractionMethod) to three procurement-oriented stages — Reading the tender document, Compliance review, Bid-ready summary — and exports a stepper, status pill, info popover (native details/summary, zero client JS), per-document badge, and in-progress callout.
  • Wired the indicator into src/components/tender/DocumentAnalysisSection.tsx: header stepper with status pill, info popover explaining the three stages in plain procurement language, per-document stage badge in the main card eyebrow and in the collapsed secondary-document rows, and an in-progress callout when the analysis is still running.
  • Copy is written for tender professionals, not developers: no mention of regex, AI models, Gemini, Python, cron jobs, or internal timestamps. The status pill, per-document badge, and in-progress callout all use procurement language (e.g. "Reading the tender document", "Compliance review in progress", "Review complete").

Platform Impact

Users on tender detail pages can now see, at a glance, whether each support document has been read, is being analysed, or has been converted into a bid-ready summary. The indicator explains the pipeline in plain procurement language so users know what to expect and when the document is ready for bid preparation, without exposing internal pipeline terminology.

2026-06-06
AI Matching & Intelligence
Tender match score now returns a meaningful partial score when eligibility fails

Clicking "Calculate Matching Score" on a tender detail page previously always returned 0% when a company failed any one of the five eligibility gates (Industry Codes, BBBEE, Company Size, Financial Requirements, CIDB Grading). The user now sees a partial 0-100% score computed from the 12 other matching factors (BBBEE match, province, value, experience, qualifications, capacity, document readiness, etc.) plus the full factor breakdown, so they can see exactly which dimension to improve. The cron batch path keeps its strict eligibility-gate semantics (ineligible pairs are still skipped).

Key Changes

  • calculateMatchForApplication() in src/lib/services/tender-matching-automation/core.ts now calls calculateEnhancedScore() with isEligible=false when the eligibility check fails, returning a partial score + full factor breakdown instead of a flat 0%. The eligibility contribution is 0; the other 12 factors still contribute. Profile-incomplete companies fall back to the eligibility-only breakdown so they see which required fields to fill in.
  • Centralized province matching in a new getProvinceMatchScore() helper exported from src/lib/services/province-service.ts (Tier 2 frozen, additive). The matching engine now delegates to this single source of truth instead of rolling its own normalization.
  • New province match outcomes — exact (100), national (100, match-any), neighbour (60, partial credit for adjacent provinces), partial_overlap (40, multi-province company with no direct match), no_tender/no_company/unknown_tender (50, neutral), mismatch (0).
  • Added city/region aliases to PROVINCE_VARIANTS: JHB, PTA, TSHWANE, DBN, ETHEKWINI, CPT, CAPE TOWN, KAAPSTAD, BLOEMFONTEIN, MANGAUNG, POLOKWANE, NELSPRUIT, MBOMBELA, KIMBERLEY, MAHIKENG, MMABATHO, BISHO, BHISHO all resolve to their parent province.
  • Added PROVINCE_NEIGHBOURS adjacency map (Gauteng → Limpopo/Mpumalanga/North West/Free State, etc.) and NATIONAL_PROVINCE_VARIANTS set (National, RSA, South Africa, ZA, All Provinces, Countrywide, Nationwide).
  • 17 new tests in src/lib/services/__tests__/province-service.test.ts covering exact / alias / city / national / neighbour / partial-overlap / mismatch outcomes, dedup, and defensive parsing of company provinces (JSON string, array, plain comma-separated string).

Platform Impact

Users clicking "Calculate Matching Score" on a tender detail page now see a meaningful 0-100% score with factor breakdown when they fail one eligibility criterion, instead of an uninformative 0%. They can identify which matching dimension is weak (e.g. industry codes, BBBEE, province) and take targeted action. Province matching is now centralized in a Tier 2 frozen service and includes city/region aliases that previously fell through to title-case, causing silent mismatches.

Related:/tenders/[id]
2026-06-06
SEO & AI Visibility
Site-wide identity bundle applied to all tender routes (XSS-safe JSON-LD)

Every Tenders-SA page now emits the canonical Organization + WebSite (SearchAction) + BreadcrumbList identity bundle via a single shared builder. Refactored the root layout and all 24 sa-tenders page templates to render JSON-LD through the XSS-safe <StructuredData> component, closing a XSS vector where unescaped user-controlled strings could break out of the script block.

Key Changes

  • Added buildTenderPageIdentityBundle() in src/lib/seo/tender-page-schemas.ts to emit a single canonical {Organization, WebSite, BreadcrumbList} triple that reuses the same @id values as the global layout — Google knowledge graph merges them into one entity.
  • Root layout (src/app/layout.tsx) now uses the same <StructuredData data={...}/> + canonical builders instead of an inline raw <script> block; the global identity and per-page identity no longer drift.
  • Applied the identity bundle to all 10 previously-uncovered tender routes: /sa-tenders/tender/[slug] (the most important SEO page), /sa-tenders/awards/organizations, /sa-tenders/organizations, all four /sa-tenders/organizations/[orgSlug]/{notices,closed,cancellations,awarded} pages, and the cancellations/notices sub-pages of categories and provinces.
  • Migrated 24 sa-tenders page files from raw <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}/> to <StructuredData data={jsonLd}/>. The new component escapes <, >, &, and \u0027 in the JSON-LD payload, closing a class of stored-XSS bugs caused by tender titles, organisation names, or category labels containing break-out characters.
  • Deduplicated the BreadcrumbList on /sa-tenders/tender/[slug] — the page now emits a single, canonical Home > Tenders > [Tender] breadcrumb generated by buildTenderBreadcrumb() instead of one hand-rolled and one identity-bundle breadcrumb sitting side by side.

Platform Impact

Establishes a single, mergeable Tenders-SA entity in Google's knowledge graph across every page; the sitelinks search box and knowledge panel signals now propagate to all tender routes. Eliminates a stored-XSS class affecting every tender, organization, category, and province page. The @graph for the tender detail page now correctly contains the platform Organization identity alongside the publisher GovernmentOrganization entity, fixing a long-standing gap where the page referenced the publisher but not the platform.

2026-06-05
SEO & AI Visibility
Google-compliant paywall schema added to company intelligence pages

Implemented Schema.org JSON-LD structured data for paywalled content on company intelligence profile pages to resolve Google Search Console's "Crawled - currently not indexed" exclusions. The schema identifies premium content via CSS selector and works alongside existing Organization schema.

Key Changes

  • Added generatePaywallSchema function that creates WebPage schema with isAccessibleForFree set to "False"
  • Integrated JSON-LD paywall schema into page component alongside existing Organization schema
  • Used Award History table container as premium content selector (.bg-white.rounded-xl.shadow-sm.border.border-gray-200.overflow-hidden.relative)
  • Schema renders server-side in initial HTML response for search crawler parsing
  • Maintains visual gating/blurring for unauthorized end-users via existing auth checks

Platform Impact

Company intelligence pages should now be properly indexed by Google as the paywall schema provides explicit signals about content accessibility, resolving indexing exclusions while maintaining user experience.

2026-06-05
AI Matching & Intelligence
Procurement Intelligence Section on category, organisation, and province listing pages

Added the ProcurementIntelligenceSection component and JSON-LD structured data (Dataset + FAQPage schemas) to all three listing page types — categories, organisations, and provinces. Free users see an upgrade prompt; Pro users see full market analysis.

Key Changes

  • src/app/sa-tenders/categories/[category]/page.tsx: Added auth + isPro check, getIntelligenceOrNull fetch, ProcurementIntelligenceSection component after pagination, and Dataset + IntelFAQ schemas appended to the JSON-LD @graph array.
  • src/app/sa-tenders/organizations/[orgSlug]/page.tsx: Added auth + isPro check, getIntelligenceOrNull fetch, ProcurementIntelligenceSection before OrgFAQ, and Dataset ("Procurement Dependency Map") + IntelFAQ schemas appended to the JSON-LD script array.
  • src/app/sa-tenders/provinces/[province]/page.tsx: Added auth + isPro check, getIntelligenceOrNull fetch, ProcurementIntelligenceSection after pagination, and Dataset ("Procurement Power Profile" with spatialCoverage) + IntelFAQ schemas appended to the JSON-LD @graph array.

Platform Impact

All three listing page types now render procurement intelligence (market overview, competitive landscape, market structure, bidder behavior, breakdown) when data is available. JSON-LD Dataset and FAQPage schemas improve SEO visibility for intelligence-rich pages.

2026-06-05
AI Matching & Intelligence
Procurement Intelligence extended to all 12 publication-type sub-pages

Embedded the ProcurementIntelligenceSection on the 12 sub-pages that drill into a specific publication type (awarded, closed, cancellations, notices) for categories, organisations, and provinces. Each sub-page now shows the same market analysis, HHI gauge, top-supplier ranking, and JSON-LD Dataset + FAQPage schemas as its parent listing page.

Key Changes

  • New shared server component src/components/intelligence/IntelligenceBlock.tsx bundles the auth + isPro lookup, cached getIntelligenceOrNull fetch, and JSON-LD Dataset + FAQPage schema emission so each sub-page only needs a single JSX line to embed the section.
  • Category sub-pages — categories/[category]/{awarded,closed,cancellations,notices}/page.tsx — render <IntelligenceBlock dimension="category" /> after pagination.
  • Organisation sub-pages — organizations/[orgSlug]/{awarded,closed,cancellations,notices}/page.tsx — render <IntelligenceBlock dimension="institution" /> inside the lg:col-span-3 main column, using the "Procurement Dependency Map" schema name.
  • Province sub-pages — provinces/[province]/{awarded,closed,cancellations,notices}/page.tsx — render <IntelligenceBlock dimension="geography" /> after pagination, with AdministrativeArea spatialCoverage on the Dataset schema and the "Procurement Power Profile" schema name.
  • Helper renders null + emits no script when no precomputed intelligence exists for the given dimension, so sub-pages stay safe to ship even if a dimension is missing from the latest cron batch.

Platform Impact

Competitive intelligence is now uniformly available across all category, organisation, and province pages — main listing AND every publication-type sub-page. Users browsing, for example, /sa-tenders/categories/construction/awarded see the same HHI concentration gauge, top-supplier ranking, and breakdown as the main /sa-tenders/categories/construction page. The shared helper keeps the per-page integration cost at one JSX block plus one import — no per-page auth, fetch, or JSON-LD boilerplate. Zero new TypeScript or lint errors introduced; 167 pre-existing errors in api-sdk-packages/cli and tmux-opencode/tools unchanged.

2026-06-05
AI Matching & Intelligence
Competitive Intelligence — cron activated & award date query fix

Activated competitive intelligence daily cron — data now populates all 15 listing pages with market metrics (HHI, CDS, supplier rankings, concentration). Fixed computation to query by tenderAward.awardDate instead of tender.publicationDate, and added createdAt fallback when publicationDate/awardDate is null. Added strict comments across all 13 competitive intelligence files to distinguish from Procurement Intelligence (news/scraper feed).

Key Changes

  • Added /api/cron/compute-intelligence to production crontab at 02:00 daily — populates competitive_intelligence table for all 384 categories x 3 time windows, 2,223 organisations x 3, and 30 provinces x 3.
  • First manual trigger completed: 384 categories, 2,223 orgs, 30 provinces computed in ~42s (cached second run: ~32s).
  • Added "COMPETITIVE INTELLIGENCE" header comments to ProcurementIntelligenceSection, IntelligenceBlock, and all 8 sub-components to prevent confusion with the Procurement Intelligence news/scraper system.
  • Added comments to cron route, computation engine, service layer, and prisma domain model clarifying this is the Category Competitive Intelligence system (spec: .kiro/specs/competitive-intelligence-embedded/).

Platform Impact

Competitive intelligence panels now render real data on all category, organisation, and province listing pages (3 main + 12 sub-pages). Free users see MarketOverview stats + top 3 suppliers; Pro users see full data. The cron maintains freshness automatically at 02:00 daily.

2026-06-05
Tender Data
Data Normalization Layer — Phase 1 + 9 foundation (entity_canonicalization_log, parseAmountOrNull, award-date-anomalies cron)

First shipping slice of the Data Normalization Layer spec. Adds the persistent entity_canonicalization_log audit table, the canonical parseAmountOrNull amount parser that never coerces 0 to a real amount, the award-date anomaly detection logic and a weekly cron route that records rejected award dates for human review. No production behavior change yet — the canonical utilities are adopted in this commit but not yet wired into the tender-persistence service (Phase 9.4 requires the Tier 2 Impact Assessment to be approved first).

Key Changes

  • New entity_canonicalization_log table (prisma/audit-domain.prisma) — the persistent replacement for the in-memory ManualReviewQueue that lost data on server restart. Used by Phase 8 (persistent manual review queue) and the OCPO spec.
  • New tender_award_date_anomalies table (prisma/audit-domain.prisma) — captures award dates rejected by the award-date-guardrails validators for admin review. Indexed by tenderAwardId, reason, and detectedAt.
  • parseAmountOrNull(raw) — the single source of truth for amount parsing. Returns null (never 0) for all 16 NULL/undisclosed sentinels and explicitly rejects 0 with reason: "zero" so callers can log to entity_canonicalization_log instead of polluting award totals. Supports R / ZAR / RAND / USD / EUR / GBP currency tokens and k / m / bn magnitude suffixes with wasInferred: true.
  • classifyAwardDate(raw, now?) — the single source of truth for award-date anomaly classification. Returns null for valid dates or { reason: "before_min" | "after_max" | "invalid" | "null" | "unknown" | "pending" } for rejected dates.
  • New weekly cron route GET /api/cron/award-date-anomalies — finds TenderAward rows outside the valid range and inserts a tender_award_date_anomalies row per anomaly. Authenticates via isValidCronSecret (no PUBLIC_API_ROUTES modification; the existing /api/cron/* wildcard at src/middleware.ts:66 covers it). Tier 1 LOCKED modules untouched.
  • NULL_UNDISCLOSED_HANDLING_TABLE — the canonical 11-row NULL/undisclosed handling table at src/lib/awards/null-undisclosed-handling.ts, reproduced exactly from the analyst report §4.1.2 (no extensions, no "Same as above" stubs).
  • isValidAwardDateCronSecret — a named cron-secret validator for the award-date-anomalies route at src/lib/cron-secrets/award-date.ts. Thin wrapper around the Tier 1 isValidCronSecret so the route can declare its secret dependency explicitly.

Platform Impact

Foundation work — no user-visible data has changed yet. Future phases (4, 2, 3) will adopt these utilities to collapse the 3+ duplicate Eskom rows in the top-companies leaderboard, stop coercing 0 amounts to real award values, and stop silently dropping award dates like 1900-01-01 from leaderboard aggregations.

2026-06-04
Performance & Reliability
Amazon SES provider — fixed orphan instance causing cron jobs to skip SES

Fixed a critical bug where EmailService created a separate uninitialized SES provider instance, causing all cron and automated email flows to skip SES entirely and fall through to SendPulse/Resend. Also fixed a cold-start race condition in SES diagnostics.

Key Changes

  • EmailService now uses the shared sesEmailProvider singleton instead of creating a new SesEmailProvider() instance that was never initialized. isProviderAvailable("ses") correctly reflects the singleton initialization state.
  • SES getDiagnostics() now routes through the standard initialize() path instead of creating a separate SESv2Client, preventing concurrent cold-start API calls to AWS from racing each other.
  • Admin dashboard and health endpoints no longer see "SES not connected/initialized" errors on cold start — the singleton initializationPromise guard properly serializes concurrent requests.
  • Cron jobs, email sequences, digest services, alert services, and all 63+ consumers of emailService.sendEmail() now correctly try SES as the first provider instead of skipping it.

Platform Impact

All automated email flows now use Amazon SES as intended instead of silently falling back to more expensive providers. Admin dashboard loads reliably on cold start without spurious SES connection errors.

Related:/admin/emails
2026-06-04
Platform Updates
Tender detail AI Document Analysis now matches the platform premium theme

The AI Document Analysis card on every tender detail page has been re-themed to match the rest of the page — green accent border, gradient header, branded icon chip, and gold chevrons on the per-document accordions. The Evaluation Criteria section now reads vertically: sub-headings stack above their content instead of being cramped into a label-on-the-left, value-on-the-right row.

Key Changes

  • src/components/tender/DocumentAnalysisSection.tsx re-themed: Card uses the same border-l-4 #007A5E accent and emerald-to-teal gradient header as TenderContactInfoCard, IssuingOrganizationCard, TenderAtGlanceCard, TenderAiSummaryCard, TenderLegislationContext and TenderRequirementsCard. The Brain icon sits inside an emerald-100 chip, and the document-count badge sits on the right of the header.
  • New SectionBlockPremium component replaces the old SECTION_COLORS palette and the purple SectionBlock / purple accordion chrome. Each section now renders as: row 1 = emerald-tinted icon chip + uppercase eyebrow + document source chip, row 2 = white rounded content surface with a thin gray border.
  • Per-document accordions (documents 2..N) re-skinned: green-emerald gradient summary bar, gold #FFB81C chevron, no more purple. Document 1 stays flat at the top of the card so the most important analysis is visible without expanding anything.
  • Empty "Analysis Pending" state re-themed to match: green border, emerald icon chip, emerald-tinted body. The legacy "Special Sections" purple treatment was removed entirely.
  • src/components/tender/StructuredEvaluationView.tsx rewritten: every sub-heading is now an uppercase eyebrow stacked above its content. The previous `min-w-[100px] shrink-0` inline label-value rows (which pushed sub-criteria headings to the right of the value and made long criterion names wrap awkwardly) are gone. Sub-criteria render as a list of `[criterion | weight pts]` rows inside a labeled "Point Allocation Breakdown" block; details render as a labeled "Notes" block; the preference-system and functionality-threshold badges remain on a single row above the sub-sections for quick scan.
  • Nested unknown fields recurse with a depth cap of 4, so a JSON blob never produces a deeply nested or visually confusing tree. StackedField + ExtraFieldContent handle primitives, arrays of primitives, arrays of objects, and arbitrary nested objects consistently.
  • TenderDocumentAnalysisCard.tsx (the alternative renderer that powers /tenders/[id]) is unchanged: StructuredEvaluationView preserves the showHeader={false} prop, so it is consumed inline exactly as before.

Platform Impact

The AI Document Analysis section on a tender detail page now reads as part of the page instead of feeling grafted on. Sub-section headings (Evaluation Criteria, Point Allocation Breakdown, Notes, and any other structured field) sit above their content, matching the rest of the page — bidders and procurement officers can scan more quickly without their eyes being forced into the awkward label-on-the-left layout. Authors of the analysis data do not need to change anything; the same TenderDocumentAnalysis / AnalysisSection JSON shape is rendered in the new chrome.

2026-06-03
SEO & AI Visibility
Tender detail AI Document Analysis no longer shows duplicate content

When multiple tender documents re-state the same information (contact details, submission deadlines, compliance clauses), the AI Document Analysis section now suppresses every duplicate after the first. Each section shown on the page is unique across the entire tender — even when 5+ documents each carry the same paragraph.

Key Changes

  • New src/lib/utils/section-dedup.ts implements n-gram shingle (n=5) + containment similarity with Jaccard as a secondary signal. Default thresholds: containment >= 0.85 OR Jaccard >= 0.80. Deterministic, zero AI cost, <1ms per comparison.
  • src/lib/tender-data-transform.ts transformDocumentAnalysis now runs the dedup at the data layer across all 5 layout components (Active/Closed/Cancelled/Awarded/Notice), ExpiredTenderContent and the alternative TenderDocumentAnalysisCard renderer — single integration point.
  • Dedup applies (a) within each document across its 7 legacy insights + extended analysisSections and (b) across all documents on the page. First occurrence wins. Metadata fields (contractType, procurementThreshold, fileName, confidence) are never deduped.
  • Suppressed sections emit as `undefined` so the existing renderers (DocumentAnalysisSection, TenderDocumentAnalysisCard) skip them with no UI changes required.
  • 21 unit tests at src/lib/utils/__tests__/section-dedup.test.ts cover: exact duplicate suppression, near-duplicate with re-statement, paraphrased re-statements, topically distinct sections preserved, source provenance preservation, short-section exemption, and monotonic threshold behaviour.

Platform Impact

Users reading a tender detail page no longer see the same paragraph two or three times under different headings. The first document to surface a piece of content wins; later documents that re-state it are cleanly suppressed. TenderDetailLayoutSelector renders less repeated content, improving signal-to-noise for bidders and procurement officers.

2026-06-03
SEO & AI Visibility
SEO pipeline unified through orchestrated AI client

All SEO generation now routes through the orchestrated aiClient (useCase: "seo") with no model pre-selection. The page no longer calls AI on the request path — TenderAnalysisRewriteService is the single source of truth for TenderSEO, and the tender detail page falls back to canonical schema fields (tender.title + tender.description).

Key Changes

  • src/lib/services/seo-generation.service.ts and src/lib/services/enhanced-seo-generation.service.ts swapped from geminiClient / enhancedAIProviderManager to aiClient.generateText(..., { useCase: "seo" }) with no model pre-selection. The orchestrator (nvidia → bedrock → gemini → cloudflare → openrouter → groq) handles provider rotation and circuit-breaker failover.
  • src/lib/services/tender-analysis-rewrite.service.ts Pass 3 SEO prompt now uses PromptEngine.generateEntitySEOPrompt({ subjectKind: "tender", ... }) — the legacy generateSeoUpdatesPrompt is preserved as a deprecated wrapper for backward compatibility.
  • src/app/sa-tenders/tender/[slug]/page.tsx no longer imports SEOGenerationService. The page reads tender.seo from the eager Prisma include and falls back to tender.title + tender.description + tender.aiSummary. The silent inline fast-path and the expired-tender hard-replacement were removed.
  • src/lib/services/gemini-analysis.service.ts no longer pre-generates SEO inline after document enrichment — that block (which silently swallowed errors via try/catch) was the original source of the off-topic description drift per reports/seo/tender-detail-description-drift-2026-06-03.md.
  • Clamp-and-sanitize helper (160 char description, 70 char title) prevents misbehaving models from breaking meta tags. buildSourceOfTruth now truncates tender.description to 4000 chars in addition to document.extractedText, so a 200k-char description cannot blow up the prompt context.
  • scripts/bulk-enhance-tender-metadata.ts and scripts/regenerate-fallback-seo.ts were broken (one called a non-existent preCacheTenderSEO, the other had swapped argument order). Both fixed.
  • scripts/test-seo-prompts.ts golden fixture runner with 10 hand-curated fixtures (scripts/fixtures/seo-tender-fixtures.json) validates the prompt pipeline end-to-end: 10 fixture cases + parser contract suite + clamp-and-sanitize units + system-prompt sanity. 13/13 checks passing.

Platform Impact

Tender page metadata is no longer at risk of being silently generated or overwritten by inline AI on the request path. SEO is now driven exclusively by the analysis rewrite pass, which uses the same aiClient orchestrator as every other AI call on the platform. Provider rotation, circuit-breaker failover, and rate limiting are uniform across the codebase.

2026-06-03
Performance & Reliability
Nav bar API status now uses v2 routes

The API status indicator on the navigation bar now fetches platform metrics through the new v2 combined-status endpoint, which uses the Worker's v2 meta status API for developer sync information.

Key Changes

  • New /api/v2/meta/combined-status public endpoint replaces the v1 route for nav bar API status.
  • Developer API health check now uses the Worker's v2 endpoint (/v2/meta/status) instead of v1.
  • Route is publicly accessible — no authentication required — so all users see accurate API health.
  • Same rich platform metrics (tenders, awards, companies, suppliers, organizations, award values) with same response shape — no frontend changes needed.

Platform Impact

The nav bar API status continues to work identically for all users, now powered by v2 infrastructure with the Worker's latest v2 meta status API.

2026-06-02
Performance & Reliability
Amazon SES adopted as primary email provider

All outbound email now routes through Amazon SES first (50,000 emails/day quota, ~$0.10/1,000 emails). SendPulse and Resend are retained as automatic fallbacks. No configuration changes required for existing users.

Key Changes

  • Amazon SES is now the primary email provider for all notification types — transactional, marketing, digests, alerts, and onboarding emails.
  • SendPulse is preserved as second-line emergency fallback. Resend is preserved as third-line fallback for critical transactional email.
  • 10 messages/second rate limiting enforced to stay within SES quotas. Bulk sends include automatic throttling.
  • Zero consumer impact — all 23 email service consumers and 4 factory consumers work identically. Only internal routing changed.
  • Email delivery reliability improved: 3-step provider fallback chain (SES → SendPulse → Resend) replaces the previous 2-provider split.

Platform Impact

Email delivery costs are reduced with AWS SES pricing. Email reliability improves with a 3-step fallback chain. No user-facing changes — email notifications continue to work identically.

2026-06-02
SEO & AI Visibility
Extended tender document analysis — 15+ section types with source provenance

Tender document analysis now classifies content into 15+ section types (up from 7). Each section shows which source document and category it came from. Unclassified content is re-classified by AI for maximum coverage.

Key Changes

  • Extended document classification from 8 to 15 regex pattern groups — CONTRACT, QUALITY, SAFETY, ENVIRONMENTAL, METHODOLOGY, EXPERIENCE, PRICING sections added.
  • Keyword-score fallback classifier catches content that regular expressions miss.
  • AI re-classification service re-classifies any remaining unclassified content into the extended taxonomy.
  • Every analysis section now shows its source document name and category for full provenance.
  • Tender detail pages (both app and SEO) display the extended sections as labelled accordion items.
  • Existing analyses can be backfilled: run `npx tsx scripts/reclassify-existing-analysis.ts`

Platform Impact

Users see richer, more accurate document analysis with clear attribution to source documents. Extended classification means fewer collapsed catch-all buckets and more precise browsing of tender requirements.

2026-06-02
Performance & Reliability
Fixed live extraction Prisma error: removed non-existent TenderDocument columns

Tender extraction was failing with a Prisma error because the integration service was attempting to write contractType and procurementThreshold to TenderDocument — columns that do not exist on the live model. Removed them from the metadata mapper. The richer fields still flow into TenderDocumentAnalysis via analysisSections / evaluationCriteria, so no data is lost.

Key Changes

  • tender-extraction-integration.service.mapExtractionMetadata now only returns documentCategory (a real TenderDocument column). contractType and procurementThreshold are no longer written to the document row, eliminating the `Unknown argument `contractType`` Prisma error in the live extraction path.

Platform Impact

Live document extraction no longer crashes mid-write. New tender documents ingest end-to-end and the immediate AI rewrite phase runs as designed.

2026-06-02
SEO & AI Visibility
Professional AI rewrite of all extended document analysis sections

After the extended-taxonomy rollout, the 15+ analysis sections on existing tender documents still showed raw extracted text on the frontend. A one-off script rewrites every section professionally; the live extraction path now rewrites every section as it is ingested.

Key Changes

  • New scripts/rewrite-extended-sections-once-off.ts — one-off resumable runner that forces the AI rewrite on every TenderDocumentAnalysis that has analysisSections. State-tracked via .rewrite-once-off-state.json. Not a cron, not in package.json scripts — invoked manually with `npx tsx -r dotenv/config scripts/rewrite-extended-sections-once-off.ts` (optionally --limit=N, --concurrency=N, --dry-run).
  • tender-analysis-rewrite.service.saveAcceptedRewrite now bumps extractionVersion to 3 whenever the input had analysisSections (or sectionTaxonomyVersion >= 2), so the version always reflects that the AI rewrite was applied. Preserves the original analysisSections array structure (with source provenance) when the AI does not echo back rewritten extended sections, so a follow-up rewrite can still see them.
  • The live extraction path in tender-extraction-integration.service.ts already calls rewriteAnalysisById(..., { source: "extraction", force: true }) immediately after every extraction, so going forward every new section is AI-rewritten before it is visible on the frontend. No live-path code change required.

Platform Impact

Tender detail pages now show professionally written, point-by-point supplier guidance for every section of every document, rather than raw sentences copied from the source PDF. Source-document provenance is preserved on every section.

2026-06-02
Company Intelligence
Government Restricted Supplier Database — now searchable on Tenders SA

Daily sync of the OCPO/CSD restricted supplier PDF (~200-500 entries). Suppliers restricted by National Treasury are now flagged in forensic analysis and compliance checks. Every company intelligence report shows an OCPO restriction badge if applicable.

Key Changes

  • New RestrictedSupplier and OcpoSyncMeta models — daily sync from CSD/OCPO PDF with SHA-256 hash-based change detection.
  • New OCPO PDF parsing endpoint in the tender-extract microservice with Camelot tabular extraction + PyMuPDF fallback.
  • OcpoSyncService — download, hash-check, parse, upsert, and match pipeline. Matches against CipcEnrichment via registrationNumber → normalizedName → pg_trgm fuzzy (0.7 threshold).
  • OCPO_RESTRICTED_SUPPLIER added as 10th forensic flag type — severity mapped from restriction reason (critical for corruption/fraud, high for defaults, medium otherwise).
  • Company intelligence page now shows an OCPO restriction badge with active/expired status, restriction details, and last-synced timestamp.
  • OcpoComplianceCheck service — dynamic DB-backed check alongside the existing static compliance checklist.
  • Public API at GET /api/v1/restricted-suppliers with search, filter, and pagination — whitelisted in middleware.
  • nt-ocpo-circulars adapter fixed to skip the RestrictedSuppliersReport PDF (no more crashes).
  • Cron route at /api/cron/ocpo-sync — authenticated via CRON_SECRET, returns sync stats.

Platform Impact

Government-restricted suppliers are now detectable on Tenders SA. Forensic analysis shows OCPO restrictions alongside the 9 existing flag types. Company intelligence pages display restriction badges. Compliance checks include OCPO as a dynamic step. The API supports programmatic access for downstream tools.

2026-06-01
Performance & Reliability
Notice pages no longer crash on incomplete tender data

Notice detail and listing pages were crashing during SSR when AI-enriched tender fields came back as undefined instead of strings. All unsafe .replace() calls across 5 notice page files are now wrapped in null-safe guards.

Key Changes

  • Added toSafeString() utility — never crashes on null/undefined, safe to use before .replace(), .toLowerCase(), etc.
  • Hardened formatPublicationType() to safely handle null/undefined publication types.
  • Fixed 5 notice listing pages where tender.publicationType.replace() was called directly without null checks.
  • Hardened getTenderSlug() in utils.ts and the notice page — title and referenceNumber are now null-safe.
  • Hardened organization province slug generation and website display for null-safe string operations.

Platform Impact

Notice pages no longer crash during SSR when AI enrichment or database fields return incomplete data. Users see gracefully defaulted values instead of a broken page.

2026-06-01
Platform Updates
Fixed ZIP extraction HTTP 500 — empty requirements no longer crash the API

Fixed HTTP 500 error when extracting ZIP archives containing only images, XML manifests, or other non-document files. The extraction microservice now returns HTTP 200 with a descriptive fallback message instead of crashing.

Key Changes

  • Removed min_length=1 constraint from ExtractResponse.requirements schema — empty requirements is a valid extraction result.
  • Added fallback requirement "No document content found in ZIP archive" in merge_extraction_results when no document entries are extractable from a ZIP.
  • Added defense-in-depth guard in main.py response construction: requirements now always defaults to a fallback if the extraction result has none.
  • Updated test_empty_list to assert fallback requirements instead of bare empty list.

Platform Impact

ZIP archives containing only images, XML manifests, or other non-document files no longer return HTTP 500. They now return HTTP 200 with a descriptive fallback message explaining why no requirements were extracted.

2026-06-01
Performance & Reliability
Company Intelligence pages — 60%+ faster with caching and query optimisation

The Company Intelligence detail pages were causing upstream timeouts (53 occurrences on May 29) due to no request caching, 10-14 sequential DB queries, and synchronous CIPC enrichment. Added ISR caching, Redis data caching, reduced query load, and nginx timeout increases.

Key Changes

  • Changed page rendering from "force-dynamic" to ISR with 1-hour revalidation — cached HTML is served for repeat visits.
  • Added Redis caching via cacheService.getOrSet for company data and CIPC enrichment results (3600s TTL).
  • Reduced award history query limit from 50 to 20 rows per page load.
  • Removed subcontractors and competitor bidders from eager-loading in the awards query — summary card queries are now leaner.
  • Increased CIPC enrichment staleness threshold from 30 to 90 days — reduces sync CIPC API calls in the request path.
  • Added nginx proxy_read_timeout 120s, proxy_connect_timeout 60s, proxy_send_timeout 60s.

Platform Impact

Company Intelligence detail pages now load faster and no longer cause upstream timeouts under peak traffic. Repeat visits within 1 hour are served from cache. Fewer DB rows are fetched per page load.

2026-06-01
Platform Updates
Fixed province count — shows 9 provinces (not 10)

The province count displayed on the Tenders by Province and Awards by Province pages incorrectly counted "National" as a province, showing 10 instead of 9. National remains available as a tender category but is no longer counted as a province.

Key Changes

  • Province count on /sa-tenders/provinces now correctly shows 9 South African provinces.
  • Province count on /sa-tenders/awards/provinces now correctly shows 9 South African provinces.
  • Province count on the SEO provinces page now correctly shows 9 South African provinces.
  • The admin sync manager description now says "9 provinces (plus National)" instead of "10 provinces".
  • National remains fully visible and clickable as a tender category — only the count was corrected.

Platform Impact

Users and search engines now see the correct count of 9 South African provinces across all province listing pages.

2026-06-01
Performance & Reliability
Fixed internal sync-export endpoints — wrong Prisma field names causing cron failures

Fixed 5 internal sync-export API routes that were failing at runtime because their Prisma select statements referenced field names that do not exist on the corresponding models, causing Cloudflare Worker data sync cron jobs to fail.

Key Changes

  • TenderDocumentAnalysis sync-export: replaced non-existent qualityScore and confidence fields with aiConfidence.
  • Tender sync-export: fixed tenderId → tender_id (correct Prisma field name for Tender model).
  • TenderAward sync-export and public-query: fixed tenderId → tender_id inside nested tender: { select } blocks.
  • TenderAward endpoints: removed non-existent category field from nested Tender select.
  • SourceOrganization sync-export: removed non-existent legalName field.

Platform Impact

Cloudflare Worker data sync cron jobs no longer fail on Prisma unknown-field errors for tender analyses, tenders, awards, and organizations endpoints.

2026-06-01
SEO & AI Visibility
AI blog generation pipeline now produces articles (fixed 100% novelty rejection rate)

The content novelty filter had a compound condition bug causing every AI-generated article to be deleted instead of saved as a draft. The filter now correctly passes articles with meaningful differentiation, and articles that still fail are tagged for review instead of deleted — eliminating wasted AI costs.

Key Changes

  • Fixed the novelty filter compound condition — the old logic rejected every article by requiring minSimilarity < 0.5, which is impossible in a procurement-specialized corpus. The fix uses score >= 40 with near-duplicate (>= 0.95) blocking only.
  • Articles that fail the novelty check are now kept as drafts with a NOVELTY_FLAGGED tag instead of being deleted — no more wasted AI generation costs.
  • Added AI topic suggestion to the cron pipeline — one Gemini call per run suggests a unique topic before falling back to the 120 template topics.
  • Added noveltyWarning to the NoveltyResult interface for admin visibility on articles with moderate similarity (score 40-69).

Platform Impact

The blog generation cron now produces articles instead of deleting them all. AI costs are no longer wasted on generated-and-deleted articles. The daily draft count now actually increments toward the daily limit.

2026-05-30
AI Matching & Intelligence
AI Assistant now works for visitors

The site-wide AI Assistant can now answer general tender, compliance, and platform questions for visitors before they sign in, while private application-assistance flows remain protected.

Key Changes

  • Visitors can now use the global AI Assistant from the floating button, Try AI button, and assistant shortcuts without being blocked by authentication.
  • The assistant now keeps guest conversations separate from private saved application data and avoids storing orphan guest chat messages.
  • Chat failures now show clearer, actionable messages for authentication blocks, rate limits, service issues, and temporary connection problems.

Platform Impact

Anonymous visitors can get useful tender guidance before creating an account, and logged-in users keep the same saved conversation and application-assistance protections.

Related:/api/ai/chat
2026-05-30
Performance & Reliability
Nav bar API Status Indicator now works for all users

The API status indicator on the navigation bar was showing false "API Issues" for unauthenticated users because the status endpoint was blocked by the authentication middleware. The route is now publicly accessible so the nav bar correctly shows API health for everyone.

Key Changes

  • The API Status Indicator on the nav bar now works for all users — previously it showed false "API Issues" for unauthenticated visitors.
  • The combined-status endpoint is now accessible without authentication, so the nav bar correctly displays API health whether you are signed in or not.

Platform Impact

All users — authenticated and anonymous — now see accurate API health status on every page instead of a misleading "API Issues" indicator.

2026-05-30
Performance & Reliability
Fixed false "API Issues" warning for visitors — API status now accurate for all users

The API status indicator in the navigation bar was showing a false "API Issues" error for unauthenticated visitors because the health endpoint was blocked by the authentication middleware. The endpoint is now accessible without login, displaying accurate platform health to all users.

Key Changes

  • Added /api/v1/meta/combined-status to the public API routes whitelist so unauthenticated visitors can access the platform health endpoint.

Platform Impact

All visitors now see accurate API health status in the navigation bar instead of a false "API Issues" warning.

2026-05-30
AI Matching & Intelligence
Evaluation Criteria now renders as formatted criteria list instead of raw JSON text

When the Gemini AI extraction returns structured JSON for evaluationCriteria (e.g., system, sub_criteria with weights, details), the data now renders as a human-readable criteria breakdown instead of raw JSON text. A source-layer guard also prevents non-string values from reaching the database.

Key Changes

  • Created shared StructuredEvaluationView component for unified structured evaluation rendering across the app.
  • Added normalizeStringField() type guard in gemini-analysis.service.ts to stringify non-string evaluationCriteria values before persisting.
  • Updated DocumentAnalysisSection.tsx to detect JSON in evaluationCriteria and render via StructuredEvaluationView instead of raw text.
  • Updated TenderDocumentAnalysisCard.tsx accordion to render evaluationCriteria as structured view when it contains parseable JSON.
  • Replaced duplicated inline structured evaluation rendering in both components with the shared component.
  • Added unit tests for normalizeStringField (7 branches) and StructuredEvaluationView (9 visual states).

Platform Impact

Structured JSON in evaluationCriteria now renders as a formatted criteria list with system badge, point allocation breakdown, and details — matching the evaluationStructured rendering quality. Plain-text evaluationCriteria is unaffected.

2026-05-30
Billing & Access
PayPal webhook reliability overhaul — zero revenue loss from unknown event types

Refactored the PayPal webhook handler from a "validate-first, reject-on-failure" architecture to an "accept-first, store-then-process" pipeline. PayPal events are now stored immediately in the database before any processing, ensuring that even unknown event types, schema variations, or signature verification failures never cause revenue loss.

Key Changes

  • All incoming PayPal webhook events are now stored as raw payloads in the database before any processing — eliminating revenue loss from strict Zod validation rejecting valid payments.
  • Unknown PayPal event types (e.g., new event types PayPal adds without notice) are accepted, stored, logged, and processed as RECEIVED — no longer returning HTTP 400.
  • Business logic moved to a new async worker that runs after the HTTP response is sent, reducing webhook response time and preventing PayPal retry storms.
  • Email notifications for payment receipts and subscription confirmations are now handled asynchronously by the background worker.
  • Every webhook event is tracked through a full lifecycle: RECEIVED → PROCESSING → PROCESSED (or FAILED), enabling replay, debugging, and audit of all payment events.
  • Signature verification failures no longer block event processing — events are stored and processed regardless, with signature issues logged for investigation.
  • Event deduplication via natural key (PayPal event_id) prevents duplicate processing at the database level.

Platform Impact

Zero revenue loss from PayPal webhook validation failures. Every event is stored before processing, enabling replay and debugging. Faster webhook response times prevent PayPal retry storms.

2026-05-30
Document Downloads
Unified document extraction — all formats now processed by dedicated microservice

Office document extraction (DOCX, DOC, XLSX, XLS, PPTX, ODT, RTF, CSV/TXT, ZIP) has been moved from the main app to the dedicated Python microservice, matching the existing PDF extraction pipeline. All document formats now benefit from the same CPU/memory isolation, consistent ExtractResponse schema, and unified format detection. Phase 5 now complete with 108-test regression suite and ODT bugfixes.

Key Changes

  • Office documents (DOCX, DOC, XLSX, XLS, PPTX, ODT, RTF, CSV/TXT, ZIP) now extract through the Python microservice instead of the main Next.js app — freeing up app resources for API requests.
  • ZIP file support added for tender bid packs containing mixed-format documents (PDF + DOCX + XLSX, etc.) with automatic recursive extraction and result merging.
  • XLSX, XLS, PPTX, ODT, RTF, and CSV/TXT format extraction added — previously only DOCX and DOC were supported for office documents.
  • Legacy .doc extraction quality improved via antiword/catdoc command-line tools in the microservice, replacing the crude binary text-scanning approach.
  • 108-test Python test suite added covering all extractors, registry dispatch, ZIP merging, and URL policy — Phase 5 complete.
  • ODT extraction fixed: ODT files now correctly route to ODT extractor; odfpy Text node content no longer silently dropped during XML traversal.

Platform Impact

Document extraction quality and format support improved across all office document types. CPU-intensive document processing is offloaded from the main app to the dedicated microservice, improving main app responsiveness.

2026-05-30
Document Downloads
Document extraction pipeline now processes all formats, not just PDF

The document extraction cron service no longer filters documents to PDF-only before sending them to the extraction microservice. Non-PDF documents (DOCX, DOC, XLSX, XLS, etc.) now reach the extractor, and nested B-BBEE local-content/HDI fields are correctly mapped from the extractor response. Ingestion now prefers R2-backed URLs over original government download URLs.

Key Changes

  • Removed PDF-only MIME filter from document-extraction-cron.service.ts direct extraction paths — all supported formats now reach the extractor.
  • Fixed B-BBEE field mapping in tender-extraction-integration.service.ts to read nested bbbee.local_content_requirement and bbbee.hdi_requirement with fallback to legacy top-level fields.
  • Updated document-processing-integration.service.ts to select r2StorageUrl, fileUrl, and downloadUrl with priority r2StorageUrl || fileUrl || downloadUrl — matching the backlog service pattern.
  • Renamed pdfUrl to documentUrl across all queue services, interfaces, and callers for clarity that the pipeline handles multiple document formats, with Redis backward compatibility for existing queue items.

Platform Impact

Non-PDF documents are no longer silently skipped by direct extraction paths. B-BBEE local-content and HDI requirements are correctly persisted. Ingestion extracts via R2-backed URLs for more reliable extraction.

2026-05-29
Platform Updates
Locale infrastructure — centralised formatting, SEO defaults, middleware, and admin locale wiring

Replaced hardcoded `en-ZA` locale strings across the entire platform with a centralised locale infrastructure. Created server-safe formatting utilities, client locale context, SEO helpers, middleware locale headers, and next-intl wiring. Removed all `en-US` anomalies. Added geo-targeting. Removed Afrikaans locale options.

Key Changes

  • Created src/lib/intl.ts with formatZAR(), formatLocalDate(), and formatLocalNumber() utilities.
  • Created src/contexts/locale-context.tsx (LocaleProvider + useLocale hook) reading from SettingsContext.
  • Wired LocaleProvider and NextIntlClientProvider into root layout; dynamic html lang from x-locale header.
  • Replaced hardcoded Intl.NumberFormat and toLocaleDateString calls across 30+ server-side files with named constants or utility wrappers.
  • Added createLocalizedMetadata(), createLocalizedAlternates(), resolveLanguage() to seo.ts.
  • Standardised all inLanguage values in structured-data.ts to DEFAULT_IN_LANGUAGE constant.
  • Fixed 4 files using en-US instead of en-ZA (alert-email, location-category-seo, template-seo, tender-sync-scheduler).
  • Extracted PayPal locale to named constant (Tier 2, zero behavioural change).
  • Added areaServed: ZA geo-targeting to Organisation JSON-LD schema.
  • Wired up next-intl infrastructure (i18n/request.ts, messages, withNextIntl plugin).
  • Added locale detection to middleware.ts (x-locale header, Content-Language, hreflang Link headers).
  • Cleaned up admin settings schema — removed af-ZA/af options; only en-ZA/en-US supported.
  • Added hreflang link tags to root layout head.

Platform Impact

All locale formatting flows from a single source of truth. Admin locale setting wired to client-side formatting. Edge-level locale headers for SEO (Content-Language, hreflang). next-intl ready for future translation work. Afrikaans locale options removed per single-language policy.

2026-05-29
Performance & Reliability
Notice pages are now stable — fixed random crashes on notice detail pages

Fixed random page crashes on individual notice detail pages (/sa-tenders/notices/[slug]) that occurred under load due to duplicate database queries, missing error handling, and unnecessary table lookups.

Key Changes

  • Wrapped notice slug resolution in React.cache() to deduplicate the 2× DB calls per request (generateMetadata + page component).
  • Added try-catch error handling with structured logging so transient database errors trigger a graceful 404 instead of crashing the page.
  • Removed wasteful tenderSlugMapping table lookup that always returned null (table exists but is never populated).
  • Added dedicated error boundary at /sa-tenders/notices/error.tsx with a Try Again button.

Platform Impact

Notice detail pages no longer crash under concurrent load or transient database issues. Users see a graceful error page with recovery option instead of a broken page.

2026-05-29
Company Intelligence
Unified company intelligence report gating — single shared access check service

All company intelligence report access points now use the same shared checkReportAccess service: the server-rendered page, the check-access API, and the generate API. De-duplicated inline access logic and fixed edge cases (isPurchased check, bundle-only users).

Key Changes

  • Rewrote checkReportAccess in report-access.service.ts as the single canonical gate, delegating to EntitlementService.canPerformAction for credit availability.
  • Updated /api/reports/generate to gate through checkReportAccess before delegating credit deduction to EntitlementService.consumeCredits.
  • Updated /api/reports/check-access to return monthlyAllowance and bundleCredits for richer UI state.
  • Added "Company Intelligence Reports" feature to Starter (5/mo), Professional (20/mo), and Enterprise (unlimited) pricing plans.
  • Added PRD-REPORT_CREDITS_5 bundle addon to pricing data.
  • Deduction priority remains: subscription monthly allowance → subscription reportCredits → bundle wallet REPORT_CREDITS.

Platform Impact

Eliminates duplicate gating logic across three code paths. Free users are correctly blocked from report generation. Paying users with any active subscription or REPORT_CREDITS bundle can generate. Previously generated reports grant access without re-consumption.

2026-05-29
Platform Updates
Homepage recent tenders now guaranteed to show results with cascading quality tiers

Fixed the homepage recent tenders section which showed no results when no tender simultaneously met all strict quality criteria. Now uses a 3-tier fallback that always returns the best available tenders.

Key Changes

  • Homepage recent tenders section now prefers analyzed tenders first, then tenders with processed documents, then any recent active tender.
  • Guarantees 6 tenders are always shown on the homepage (or as many as exist).

Platform Impact

The homepage always displays recent tender opportunities, eliminating the empty-state issue where overly strict filters prevented any results from showing.

Related:/
2026-05-28
Platform Updates
Compact nav bar redesign — dropdown menus for Tenders and Awards, icon-only shortcuts, overflow menu

Redesigned the main navigation shortcuts to eliminate horizontal overflow. Converted Tenders to a full dropdown with status + browse sections, added a new Awards dropdown focused on award pages, moved Changelog to icon-only, and introduced a "More" overflow dropdown for secondary tools and resources.

Key Changes

  • Tenders shortcut converted to dropdown with Browse All, Closed, Awarded, Notices, Cancellations (status) and By Category, Province, Organization (browse) sections.
  • New Awards dropdown with links to All Awarded Tenders, by Province, by Category, and by Organization.
  • Changelog reduced to icon-only with tooltip (was button with text label).
  • Added "More" overflow dropdown for Provincial Heatmap, BBBEE/CIDB/Compliance calculators, Value Estimator, Services, Docs, and Support.
  • Reduced padding across all shortcut buttons (px-3 → px-2/px-2.5) for a tighter fit.
  • Tenders and Intelligence labels hide on screens < 1280px to save horizontal space.

Platform Impact

Navigation bar now fits comfortably on all desktop screen sizes without horizontal scrolling. All previously visible items remain accessible through dropdowns or icon-only shortcuts.

2026-05-28
Platform Updates
Dynamic multi-type leaderboards implemented — department, municipality, SOE, sector, and B-BBEE contributor level queries

Fully implemented the data-access layer for all 6 leaderboard types, resolving all cross-linking bugs, dynamic routing limitations, and table column alignment issues.

Key Changes

  • Implemented robust in-memory grouping, sorting, and aggregation in the data-access layer for all non-supplier leaderboard types.
  • Added dynamic column alignment in the LeaderboardTable component to correctly render stats for departments, municipalities, SOEs, sectors, and B-BBEE levels.
  • Removed the supplier-spend redirection barrier in the detail page, letting users load and explore all leaderboard types dynamically.
  • Made page titles, descriptions, and summaries fully dynamic based on the active type, with optimized SEO metadata.
  • Linked all types directly in the main /leaderboard page sidebar and removed all Soon badges.
  • Updated the JSON export API route to support type-specific data exports.

Platform Impact

Users can now dynamically view, analyze, and export government procurement data by supplier spend, department spend, municipal awards, SOE procurement, sector trends, and B-BBEE contributor level across South African provinces.

2026-05-28
Platform Updates
Leaderboard pages integrated into platform navigation — sitemaps, cross-linking, hub pages

The Procurement Awards Leaderboard (/leaderboard/) and Company Intelligence Leaderboard (/tools/company-intelligence/leaderboard/) pages are now fully integrated into the platform with proper navigation links, breadcrumbs, sitemaps, and SEO cross-linking.

Key Changes

  • Created root sitemap.ts aggregating all pages including both leaderboard systems
  • Replaced /leaderboard redirect with proper hub page showing stats, province grid, and leaderboard type sidebar
  • Replaced /tools/company-intelligence/leaderboard redirect with proper hub page
  • Added Procurement Awards Leaderboard link to navigation dropdown, footer, and nav shortcuts
  • Added full cross-linking between company-intelligence and procurement-awards leaderboards
  • Added breadcrumb navigation and cross-province navigation on province leaderboard pages
  • Created /leaderboard sitemap covering all province/type/period combinations
  • Added "View full national leaderboard" CTA on company-intelligence landing page
  • Updated JSON-LD schema markup with proper breadcrumb and collection page schemas

Platform Impact

Both leaderboard systems are now discoverable through navigation, search engines (sitemaps), and internal cross-links. Users can navigate between company intelligence filtering and procurement awards deep-dive pages.

2026-05-28
Platform Updates
Fixed homepage statistics: "Tender Awards" and "Awarded Companies" now reflect correct data

Corrected two front-page statistics that had their underlying queries swapped. "Tender Awards" now counts individual award line items, and "Awarded Companies" now counts unique suppliers that have won awards.

Key Changes

  • "Tender Awards" now uses the total count of individual award records (TenderAward table) instead of unique tenders with awarded status.
  • "Awarded Companies" now uses distinct supplier names from award records instead of the total award count, which overcounted repeat winners.
  • Removed the now-unnecessary query for tenders with status AWARDED.

Platform Impact

Homepage and stats-bar figures now accurately distinguish between number of awards made and number of unique companies that have won them.

Related:/
2026-05-28
Performance & Reliability
Fixed navigation corruption when visiting Closed Tenders, Notices, Awards, or Cancellations pages

Resolved a client-side router corruption bug where navigating to certain tender listing pages (Closed, Notices, Awards, Cancellations) via the nav menu would break subsequent navigation to Tenders and Awards dropdown items until a full page reload.

Key Changes

  • Added try-catch fallback to getCachedPublicationTypeCounts() on all 5 tender listing pages (Tenders, Closed, Awards, Notices, Cancellations) so transient database or cache failures do not cause server component rendering errors.
  • Added error.tsx error boundary at sa-tenders/tenders/ level to catch any remaining RSC errors gracefully and prevent Next.js router state corruption.

Platform Impact

Navigation no longer breaks after visiting tender listing pages. Users can freely browse between Tenders, Closed, Awards, Notices, and Cancellations without needing to reload the page.

2026-05-28
SEO & AI Visibility
Dedicated awards landing pages — provincial, category, and organisational award directories

Created three dedicated award-browsing landing pages that show contract awards grouped by province, category, and issuing organisation — replacing generic tender-browse links in the Awards dropdown menu.

Key Changes

  • New /sa-tenders/awards/provinces page: province grid showing AWARD_NOTICE counts per province, linking to /sa-tenders/provinces/{slug}/awarded.
  • New /sa-tenders/awards/categories page: category cards with award counts, linking to /sa-tenders/categories/{slug}/awarded.
  • New /sa-tenders/awards/organizations page: organisations grouped by type (SOEs, municipalities, national/provincial depts) with award counts, linking to /sa-tenders/organizations/{slug}/awarded.
  • Awards dropdown menu in nav-shortcuts now points to the dedicated award pages instead of the general tender browse pages.

Platform Impact

Users browsing the Awards dropdown now land on awards-focused landing pages showing contract award counts instead of general tender listing pages. Each new page is fully SEO-indexable with structured data and breadcrumbs.

2026-05-27
Platform Updates
Widget embed codes fixed — correct CDN scripts and data attributes for each widget

The widget builder on the Publishers Hub now generates correct embed snippets. Each widget uses its own data attribute and CDN-hosted script matching the published npm packages. The undocumented unified data-tendersa-widget approach has been replaced with per-widget attributes that actually auto-initialize.

Key Changes

  • Heatmap widget snippet now uses data-tendersa-heatmap and loads from unpkg (@tenders-sa-org/widget-heatmap).
  • Winners Feed snippet now uses data-tendersa-winners-feed and loads from unpkg (@tenders-sa-org/widget-winners-feed).
  • Top Companies snippet now uses data-tendersa-top-companies and loads from unpkg (@tenders-sa-org/widget-top-companies). Supports data-province, data-category, and data-limit attributes.
  • Sector Trends snippet now uses data-tendersa-sector-trends and loads from jsDelivr (@tenders-sa-org/widget-sector-trends). Supports data-province, data-category, and data-limit attributes.
  • Data attributes are scoped per-widget — unsupported attributes no longer appear on widget snippets that do not use them.
  • WordPress shortcode simplified to [tendersa_widget type="..."] — the plugin handles all rendering.
  • Developer docs now list all 4 published widget packages with GitHub links in the SDKs section.
  • FAQ now correctly references @tenders-sa-org/widget-* packages instead of the non-existent @tenderssa/widget.

Platform Impact

Publishers copying embed code from the widget builder now get working snippets that render immediately. The correct data attributes trigger auto-initialization, and CDN scripts load from the same npm packages available to developers.

2026-05-27
Performance & Reliability
Developer API sync pipeline restored — data freshness fix

Fixed a critical authentication blocker that prevented the Developer API (api.tenders-sa.org) from syncing any data from the main platform. All six sync pipelines (tenders, awards, organizations, analyses, estimates, subcontractors) were returning zero records due to the Edge Middleware blocking the shared-secret auth. Also fixed incorrect Prisma field selections causing undefined values in the tenders export, and reduced cron frequency to lower main app load.

Key Changes

  • Developer API Worker can now authenticate with the main app internal sync endpoints — the Edge Middleware was intercepting all /api/v1/internal/* requests and returning 401 because the shared-secret Bearer token was not recognized as a valid JWT.
  • Tenders sync-export route now selects correct Prisma model fields (publicationDate, estimatedValue, referenceNumber, sourceOrganization, sourceUrl) instead of six non-existent fields that produced undefined values.
  • Cron sync frequency reduced from every 5 minutes to every 30 minutes — the original 5-minute cron was making ~432 API calls/hour unnecessarily.
  • All six cron sync functions now retry on transient 5xx or network errors instead of breaking on the first non-OK response (2 retries with exponential backoff).
  • API key authentication cache now uses per-key expiry instead of a global 5-minute purge, preventing unnecessary D1 lookups for recently verified keys.
  • Corrected MAIN_APP_URL default from www.tender-sa.org (singular) to tenders-sa.org (plural) in the Worker configuration.

Platform Impact

The Developer API now correctly syncs live tender, award, organization, analysis, estimate, and subcontractor data from the main platform. Users querying the API will receive current data instead of empty or stale responses. Reduced main app load from excessive cron polling.

2026-05-27
Platform Updates
Embeddable widgets published to npm — add procurement data to any site

All 5 embeddable Tenders-SA widgets are now published on npm under the @tenders-sa-org scope alongside the SDK and CLI. You can now add procurement intelligence, winners feeds, company rankings, sector trends, and provincial heatmaps to any website with a simple npm install and script tag.

Key Changes

  • Widget Intelli (intel) at @tenders-sa-org/widget-intel — real-time procurement intelligence feed embed.
  • Winners Feed at @tenders-sa-org/widget-winners-feed — awarded tender announcements for any site.
  • Top Companies at @tenders-sa-org/widget-top-companies — ranked supplier leaderboard embed.
  • Sector Trends at @tenders-sa-org/widget-sector-trends — procurement activity by sector visualization.
  • Heatmap at @tenders-sa-org/widget-heatmap — provincial tender density map embed.
  • TypeScript SDK at @tenders-sa-org/sdk-js — zero-dependency client with full type definitions.
  • CLI tool at @tenders-sa-org/cli — terminal access to all 19+ API endpoints.
  • All widgets available via unpkg/jsdilvr CDN for script-tag installation, replacing the previous cdn.tenders-sa.org URLs.

Platform Impact

Developers and website owners can now embed live Tenders-SA procurement data into any webpage, CMS, or application using npm packages or CDN script tags — no account required.

2026-05-27
Company Intelligence
Paywalled company profile claiming with proof upload and PayPal

Introduced a paywalled company profile claiming workflow on tool pages — users pay R1,500 via PayPal to claim ownership, upload proof documents, and await admin review.

Key Changes

  • Users can initiate a claim directly from any unclaimed company profile via the "Claim This Profile" CTA.
  • Multi-step workflow: initiate claim → upload proof of ownership (CIPC, letterhead, etc.) → pay R1,500 via PayPal → confirmation.
  • Admin review endpoint allows approve/reject with notes and tracks reviewer identity.
  • Proof documents are uploaded via the existing file upload component and stored server-side.
  • PayPal order is created with custom return/cancel URLs specific to company claim flow.
  • Payment capture happens on return from PayPal; claim moves to PENDING_REVIEW on success.
  • Rejected claims can be re-submitted; approved claims lock the profile.

Platform Impact

Companies can now claim and verify their profile on Tenders-SA, adding trust and enabling profile management for verified representatives.

2026-05-26
Platform Updates
Amazon Bedrock provider with Nova Lite support

Added Amazon Bedrock as a new AI provider with Nova Lite model support for cost-efficient summarization, structured JSON extraction, and bulk processing. Bedrock is now the second option in the AI fallback chain.

Key Changes

  • New Bedrock AI provider registered in the AIClient orchestrator — positioned after NVIDIA and before Gemini in the default fallback order.
  • Amazon Nova Lite configured as primary model with low temperature (0.1) for deterministic JSON extraction.
  • Nova Micro fallback model for additional reliability when Nova Lite is throttled.
  • AWS SDK v3 Bedrock Runtime Converse API integration with streaming support.
  • Per-provider circuit breaker integration — Bedrock gets 60s cooldown on rate limits/timeouts/5xx.
  • Environment variables: AMAZON_BEDROCK_API_KEY (single key auth) or AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY (IAM auth), AWS_REGION, BEDROCK_TIMEOUT_MS, BEDROCK_DEFAULT_MODELS.

Platform Impact

AI inference is more resilient with an additional provider in the fallback chain. Nova Lite provides cost-efficient processing for bulk summarization and extraction workloads.

2026-05-26
AI Matching & Intelligence
AI Document Analysis now visible on all tender detail pages

AI-powered document analysis results now display on expired, awarded, closed, cancelled, and notice tender pages — not only active tenders. Partial results are shown; completed sections appear even when some fields are still being processed.

Key Changes

  • Expired tender pages now render document analysis results — previously the early return for expired tenders bypassed all layout rendering.
  • Awarded, closed, cancelled, and notice tender layouts now include the document analysis section — previously only ActiveTenderLayout showed it.
  • Important Dates and Contact Information fields are now rendered in the analysis cards — previously they were fetched from the database but never displayed to users.
  • Added shared DocumentAnalysisSection component used consistently across all 6 layout variants, reducing code duplication and ensuring uniform behavior.

Platform Impact

Users browsing any tender — expired, awarded, closed, cancelled, or notice — can now see AI-extracted submission guidelines, evaluation criteria, technical specifications, financial requirements, compliance requirements, important dates, and contact information without switching to a different page.

2026-05-26
SEO & AI Visibility
Fixed social share images (OG images) for categories, provinces, organizations, and authors

Fixed a crash preventing social share image generation on category, province, organization, and blog author pages. When shared on social media, these URLs were showing blank preview cards.

Key Changes

  • Fixed crash in 8 OpenGraph image route handlers — Next.js 15 requires params to be awaited as a Promise; 6 routes were destructuring without await, causing undefined params and crashing the image generator.
  • Added empty-string guard in slug-to-label conversion — consecutive or leading/trailing hyphens in slugs no longer crash the uppercase call.

Platform Impact

Social media previews now work correctly for category, province, organization, and author pages. Previously these shared as blank/error cards.

2026-05-26
Tender Data
Fixed AI document analysis not saving — missing fields on fallback path

Fixed a bug where AI-generated document analysis results were never saved to the database on the fallback analysis path, causing analysis sections to appear empty on tender detail pages.

Key Changes

  • Fallback AI analysis now correctly includes documentId (required field) — previously the create call failed silently without it.
  • Fixed wrong field names in the fallback create call — submissionRequirements, timeline, and additionalNotes were mapped to model fields that do not exist on TenderDocumentAnalysis.
  • Submission requirements and important dates are now correctly stored in the submissionGuidelines and importantDates fields.
  • Tender persistence service now automatically resolves documentId from an existing document if not explicitly provided.

Platform Impact

AI document analysis results now show up on tender detail pages that rely on the fallback analysis path. Analysis data is properly persisted and visible.

2026-05-25
Billing & Access
PayPal webhook fix: subscription activation is now working

Fixed a critical webhook URL misconfiguration that prevented all PayPal subscription activations since launch. Added payment monitoring, admin dashboard, and automated smoke testing.

Key Changes

  • Fixed PayPal webhook URL — was pointing to http://tenders-sa.org instead of the correct https://www.tenders-sa.org/api/webhooks/paypal. Zero webhook events ever reached the handler.
  • Fixed Next.js trailing-slash redirect (308) blocking PayPal webhook delivery — PayPal does not follow redirects. Added middleware exclusion for /api/webhooks to bypass the redirect entirely.
  • Fixed atomic create-before-cancel for subscription upgrades — new subscription is created first, then old one is cancelled, eliminating the dead period where users lose access.
  • Fixed race condition between verify-subscription endpoint and webhook activation — verify-subscription now uses conditional updateMany so duplicate activation is a no-op.
  • Fixed hardcoded 30-day billing period in webhook handler — now correctly calculates period end based on billingCycle (monthly vs yearly).
  • Added PAYMENT.SALE.REVERSED webhook handler for refund/reversal scenarios.
  • Rewrote payment success page with accurate error/success states — no more fake "Welcome" when verification fails.
  • Expired 3 stuck APPROVAL_PENDING subscriptions via repair script (PayPal returned 404 — users were never charged).

Platform Impact

Subscription activation is now working end-to-end. Users who complete PayPal checkout are immediately subscribed. Estimated missed MRR: R4,997/month from 3 stuck users.

2026-05-25
Billing & Access
Admin payment dashboard — subscription visibility & monitoring

New admin dashboard for managing subscriptions, invoices, and payment health monitoring across the platform.

Key Changes

  • Admin payment overview at /admin/payments with summary cards (active subs, pending, MRR, failed payments).
  • Paginated subscription list with status filtering and real-time PayPal sync per subscription.
  • Subscription detail page showing invoices, user info, plan details, and PayPal sync + cancel/delete in a danger zone.
  • Paginated invoice list at /admin/payments/invoices with amount and status display.
  • All admin payment endpoints wrapped in withAdminAuth middleware.

Platform Impact

Admin team can now monitor subscription health, manually sync subscriptions with PayPal, and view payment history without database access.

2026-05-25
Billing & Access
Payment health monitoring cron job

Automated cron endpoint that detects stuck subscriptions and alerts when activation rates drop below thresholds.

Key Changes

  • New cron endpoint GET /api/cron/payment-health — checks subscriptions created in the last N hours.
  • Detects APPROVAL_PENDING subscriptions stuck for >30 minutes and verifies real status via PayPal API.
  • Alerts via structured log when activation rate is <50% (warning) or <10% (critical).
  • Reports total MRR, active subscription count, and pending count.
  • Helper script: npm run payment:health for manual invocation.

Platform Impact

Platform operators are now alerted within 30 minutes if a payment flow breaks, preventing prolonged silent failures.

2026-05-25
Billing & Access
PayPal reliability fixes: server-side subscription verification, CSP headers, and error sanitization

Fixed several PayPal integration issues including missing server-side subscription verification after redirect, Content Security Policy blocking PayPal SDK, and internal error message leakage.

Key Changes

  • Added server-side subscription verification endpoint that checks PayPal API directly after redirect — subscription activates immediately instead of relying solely on webhook timing.
  • Increased payment success page polling from 10 retries (20s) to 30 retries (60s) with server-side verification as the first attempt.
  • Added Content Security Policy headers with PayPal domains (script-src, frame-src, form-action) to prevent PayPal SDK PaymentRequest failures.
  • Fixed internal error message leakage in PayPal update, cancel, and subscription API routes.
  • Deprecated dead proxy.ts middleware (CSP was defined but never served).
  • Deprecated unused generateCSPHeader() helper — CSP now lives in the canonical middleware.

Platform Impact

More reliable PayPal checkout — subscriptions activate faster after approval, PayPal SDK loads without CSP errors, and internal error details are no longer exposed to end users.

2026-05-25
Platform Updates
Developer API SDKs v0.1 — TypeScript, Python & CLI

Released official SDK packages for the Tenders-SA Developer API across TypeScript/JavaScript, Python, and CLI — enabling idiomatic access to all 19+ enriched endpoints covering tenders, awards, companies, organizations, analysis, and metadata.

Key Changes

  • [TypeScript SDK (@tenders-sa-org/sdk-js)](https://github.com/Tenders-SA/js) — zero-dependency client with native fetch, full TypeScript types for all 45+ response shapes, exponential backoff retry, rate limit header parsing, 5 resource classes, and paginated async iteration.
  • [Python SDK (tendersa-sdk)](https://github.com/Tenders-SA/python) — async httpx-based client with automatic camelCase-to-snake_case key conversion, 9 error classes mapped to HTTP status codes, and paginated async iteration.
  • [CLI tool (@tenders-sa-org/cli)](https://github.com/Tenders-SA/cli) — Commander.js-based CLI with 15 subcommands across 5 resources, config file at ~/.tendersa/config.json, table-formatted output, and API key management.
  • OpenAPI 3.1 specification at docs/openapi/v1.yaml — 19 endpoint paths, 45 component schemas, 6 API groups, and full security scheme definitions.

Platform Impact

Developers can now interact with the Tenders-SA platform programmatically through idiomatic SDKs in their language of choice, reducing boilerplate and enabling faster integration.

2026-05-25
SEO & AI Visibility
OG Images & SEO for Awarded, Closed, Cancellations & Notices

Added social preview images, Twitter cards, and dedicated SEO detail pages for all tender status sections. Awarded tender detail pages now display rich OG images when shared on LinkedIn, X/Twitter, and WhatsApp.

Key Changes

  • Awarded tender detail pages now include Open Graph images + Twitter summary cards using the same /api/og/tenders/[id] branded preview as active/closed tenders.
  • Awarded and closed tender listing pages now include Twitter card metadata for richer social sharing.
  • Created dedicated SEO slug routes for cancelled tenders at /sa-tenders/cancellations/[slug] with proper OG images, schema.org markup, and metadata.
  • Created dedicated SEO slug routes for tender notices at /sa-tenders/notices/[slug] with proper OG images, schema.org markup, and metadata.
  • Cancelled and notices listing pages now link to their dedicated detail routes instead of the generic tender route.

Platform Impact

Awarded, closed, cancelled, and notice tender pages now have full SEO metadata with branded social preview images. This improves click-through rates from social media, search engine indexing, and overall discoverability of status-specific tender content.

2026-05-25
Platform Updates
Developer Portal v0.1 — API keys management and public developer docs

Launched the Developer Portal with two sections: a dashboard interface for managing API keys and tracking usage, and a public documentation portal with a full API reference, tutorials, SDK listings, and changelog.

Key Changes

  • Dashboard Developer section at /dashboard/developer/ — landing page with quick-start code examples, API key overview, and usage summary cards.
  • API key management at /dashboard/developer/api-keys/ — create, list, and revoke keys. Full key shown exactly once on creation with a warning to store it securely.
  • Key detail pages at /dashboard/developer/api-keys/[id]/ — daily and monthly usage progress bars with reset timestamps, key metadata, and revoke action.
  • Public Developer Portal at /developers/ — landing page with hero, feature cards, and a 4-step getting started guide covering API key acquisition, base URL, authentication, and rate limits.
  • API Reference at /developers/reference/ — complete documentation for all 19 API endpoints across 6 resource groups (tenders, awards, companies, organizations, analysis, meta) with collapsible parameters, code examples in curl/JavaScript/Python, and response schemas.
  • Tutorials at /developers/tutorials/ — 4 step-by-step guides with runnable code: find matching tenders, company intelligence, award analytics, and document analysis.
  • SDKs & Libraries at /developers/sdks/ — status matrix for TypeScript (planned), Python (planned), PHP (community), and Go (community) clients.
  • API Changelog at /developers/changelog/ — versioned history from v1.0.0 through v1.2.0.
  • Dashboard sidebar now includes a "Developers" nav item. Footer includes an "API" link under Product.

Platform Impact

Professional and Enterprise subscribers can now create and manage API keys directly from the dashboard. Developers can explore the full API capabilities through the public documentation portal without needing an account.

2026-05-25
Platform Updates
Developer API remediation — subcontractors, pagination, meta endpoints, cron sync

Closed 9 gaps identified in the enriched API v0.1 launch: award subcontractors, paginated company awards, director cross-links, company search by category, missing cron syncs for enriched tables, enriched value estimates on tender detail, meta endpoints, and sparse field support.

Key Changes

  • Award responses now include subcontractor details (name, percentage, B-BBEE level, demographics) when available.
  • Company profiles now support paginated award history via awardsPage/awardsPageSize parameters.
  • Company profiles include director cross-links (name, role, appointment date) when CIPC data is synced.
  • Company search now supports filtering by category in addition to enterpriseType, beeLevel, and province.
  • Cron sync now refreshes organizations, tender analyses, and value estimates from the main app every 30 minutes.
  • GET /v1/tenders/{id} now includes enriched value estimates (min/max/median/confidence/methodology) from api_value_estimates.
  • New meta endpoints: /v1/meta/status (health + last sync), /v1/meta/provinces (tender counts), /v1/meta/usage (key usage stats).
  • List and search endpoints support the fields parameter for sparse responses.
  • Award analytics now uses the named CACHE_TTL.AWARD_ANALYTICS constant instead of a hardcoded value.

Platform Impact

All spec-defined response contracts from the enriched v0.1 API are now fully fulfilled. No gaps remain between the requirements document and the implementation.

2026-05-25
Performance & Reliability
Fixed page crashes on Closed Tenders, Cancellations, Notices & Awarded pages

Four tender listing pages were missing error handling and caching, causing crashes on database hiccups. Now they gracefully degrade and share cached publication count data.

Key Changes

  • Added try/catch error handling with structured logging to Closed, Cancelled, Notice, and Awarded tender listing pages — database failures now show empty states instead of crashing.
  • Consolidated 5 copies of getPublicationTypeCounts() into a single shared Redis-cached function, reducing database load by up to 25 count queries per page load cycle.
  • Active Tenders page now uses the same shared cached counts function for consistency.

Platform Impact

Users can browse Closed, Cancelled, Notice, and Awarded tender listings reliably even during transient database issues. Faster page loads from reduced duplicate queries.

2026-05-25
Billing & Access
Fixed PayPal subscription activation — payments now activate correctly

Fixed a critical issue where PayPal subscription payments were never activating after approval. The payment success page now shows accurate status instead of a false confirmation.

Key Changes

  • Fixed PayPal webhook delivery so subscription activation and billing events are processed correctly after payment.
  • Replaced the payment success page with proper loading, error, and confirmation states — you now see the actual payment status instead of a generic welcome message.

Platform Impact

PayPal subscriptions now activate properly after payment. The payment success page accurately reflects whether your payment was activated.

2026-05-24
AI Matching & Intelligence
AI Document Analysis moved higher on tender pages with professional list formatting

The AI-powered document analysis section now appears directly below the tender description so you see actionable insights sooner. Bullet points and numbered action items now render as proper lists instead of dense paragraphs.

Key Changes

  • AI Document Analysis section moved from deep below Submission Requirements to immediately after the Tender Description — putting key insights at eye level.
  • Bullet point and numbered list items inside analysis text now display as properly formatted HTML lists instead of run-together paragraphs.
  • Blank lines between sections now create visible paragraph spacing for easier scanning.

Platform Impact

Bidders can now find and read AI-extracted submission guidelines, evaluation criteria, and compliance requirements faster and with clearer formatting — reducing the risk of missing action items buried in dense text.

2026-05-24
Platform Updates
Developer API now serves enriched company and procurement intelligence

The Developer API has been rebuilt from a basic government tender mirror into an enriched intelligence platform — serving B-BBEE company profiles, AI document analysis, computed value estimates, buyer organization intelligence, and award analytics that no government API provides.

Key Changes

  • Company profiles now include enterprise type (EME/QSE/Large), B-BBEE level, CIDB grading, total award value, categories, provinces, compliance scores, and contact information.
  • Buyer organization profiles show department/municipality/SOE classification, Google ratings, Wikidata descriptions, and multi-source enrichment audit trails.
  • AI-powered tender document analysis exposes structured submission guidelines, evaluation criteria, technical specifications, financial requirements, and compliance requirements.
  • Computed value estimates provide min/median/max ranges with confidence scores and methodology (historical/document/benchmark/hybrid).
  • Award analytics deliver aggregated statistics by enterprise type, B-BBEE level, province, category, and time period.
  • Company search enables filtering by enterprise type, B-BBEE level, province, and category.

Platform Impact

Developers and data analysts can now access Tenders-SA's enriched procurement intelligence — B-BBEE enrichment, AI analysis, and computed market data — through a dedicated API, without needing to scrape the government's basic OCDS feed.

Related:/developers
2026-05-23
Performance & Reliability
Fixed content generation crashes and improved tender sync capacity

Fixed two content generation crash scenarios — empty tender datasets and duplicate fallback topic titles — while expanding tender sync capacity and adding expiration tracking to data export jobs.

Key Changes

  • Content generation no longer fails with "Cannot read properties of undefined" when a province or category has no tenders to ground the article on.
  • Fallback topic titles with date suffixes no longer cause unique-constraint crashes when the same dated title already exists.
  • Tender OCDS sync cron now pulls up to 5,000 records per run (was 2,000), reducing how often tenders are missed during ingestion.
  • Data export jobs now include an expiration date field, letting the system clean up old exports automatically.
  • Data export service migrated from legacy console bridge to structured application logger for better observability.

Platform Impact

Fewer content generation failures means more procurement blog articles reach publication. Expanded sync capacity catches more tenders on each cron pass. Data exports are now observable and auto-cleanup works correctly.

2026-05-23
AI Matching & Intelligence
Fixed AI Match Analysis auto-load and recommendations on tender detail pages

The AI Match Analysis card in the tender sidebar no longer requires a manual "Check Match" click for authenticated users. The matching score now loads automatically on page load, and the enhanced matching engine returns actionable recommendations alongside the score.

Key Changes

  • Auth session now returns a token on page load, enabling client components to make authenticated API calls immediately.
  • AI Match Analysis card auto-fetches your matching score without manual button clicks.
  • Recommendations now display correctly after running "Calculate Match Score" with the enhanced matching engine.
  • All sidebar components now use cookie-based authentication (credentials: include) instead of manually constructed Authorization headers.

Platform Impact

Faster, friction-free AI matching experience. Users see their match score as soon as the page loads instead of needing an extra click.

2026-05-23
SEO & AI Visibility
Reduced content generation novelty rejections — relaxed similarity thresholds

The hourly content generation cron was rejecting many articles as "100% similar" to existing content, wasting AI generation credits. The Jaccard similarity thresholds were too strict for a finite topic space. Relaxed title threshold from 0.72 to 0.80, intro from 0.65 to 0.72, and minimumNoveltyScore from 75 to 70. This reduces false rejections while still catching truly duplicate content.

Key Changes

  • Raised title similarity threshold from 72% to 80% before flagging as duplicate.
  • Raised intro similarity threshold from 65% to 72%.
  • Lowered minimum novelty score from 75/100 to 70/100.

Platform Impact

Fewer AI-generated articles are rejected and deleted for moderate similarity. True duplicates (80%+ title overlap) are still caught. More content reaches publication without increasing quality risk.

2026-05-23
AI Matching & Intelligence
Improved AI provider reliability — faster fallbacks and circuit breaker tuning

The AI provider fallback chain had Cloudflare first (which frequently hits daily quota), 120-second per-provider timeouts causing 4+ minute cascading delays, and a circuit breaker that only triggered on 429 errors. Reordered providers to try Gemini first and reduced timeouts to 60s. The circuit breaker now also opens on timeouts and 5xx errors with a 30s cooldown.

Key Changes

  • Reordered AI provider chain to Gemini → Cloudflare → Nvidia → OpenRouter → Groq (was Cloudflare first).
  • Reduced default provider timeout from 120s to 60s (configurable via AI_PROVIDER_TIMEOUT_MS).
  • Extended ProviderCircuitBreaker to open on timeout (AbortError) and 502/503/504 errors with 30s cooldown.
  • Applied consistent circuit breaker logic in both generateText() and chat() methods.

Platform Impact

Most AI requests now succeed on first attempt via Gemini instead of cascading through failed Cloudflare. Fallback chain completes in ~60s instead of ~4min when providers are degraded. Circuit breaker prevents repeatedly hammering failing providers.

2026-05-23
AI Matching & Intelligence
Reduced AI enhancement queue failures — backfill quality gate uses lower threshold

The AI enhancement pipeline was rejecting backfill documents with very low confidence scores (as low as 0.1), then requeueing them up to 3 times before sending to the dead letter queue — wasting AI credits. The quality gate now uses a lower threshold (0.40) for backfill source documents and sends failed backfill items directly to the DLQ without retrying.

Key Changes

  • Added BACKFILL_CONFIDENCE_THRESHOLD (0.40) constant for backfill differentiation.
  • Updated scoreRewriteQuality() to accept source context and use source-aware thresholds.
  • Backfill items that fail the quality gate now go directly to DLQ instead of 3 wasteful retries.

Platform Impact

Reduces failed AI enhancement API calls by skipping doomed retries on low-quality backfill documents. Allows more backfill output through the quality gate with the lower confidence floor.

2026-05-23
Performance & Reliability
Fixed admin analytics dashboard — heatmaps and overview now load real data

The admin analytics dashboard had a field name mismatch where Prisma queries used createdAt instead of timestamp on the AnalyticsEvent model. This fix restores heatmap data, unique session counts, conversion metrics, and the professional services payment funnel to live data instead of fallback/error states.

Key Changes

  • Replaced 3 instances of createdAt with timestamp in analytics event count, group-by, and distinct-session queries.
  • Admin analytics heatmap and overview API routes now return real event data instead of 500 errors.

Platform Impact

Admin users can once again view accurate heatmap analytics, visitor metrics, and event-backed conversion funnel data. The overview dashboard no longer silently falls back to placeholder data.

2026-05-23
User Dashboard
Supplier Intelligence widget now offers free forensic checks on dashboard

The Supplier Intelligence dashboard widget now works for all users, with 3 free forensic checks per month before upgrade gating kicks in. The widget spans full width and links directly to the tool.

Key Changes

  • Fixed the dashboard widget so it links to the working forensic analysis tool instead of a broken page.
  • Introduced usage-based gating: free users get 3 forensic analysis uses per month before being prompted to upgrade.
  • Redesigned the widget as a full-width card with live remaining-usage counter and contextual badges.

Platform Impact

Free-tier users can now try supplier intelligence and forensic risk checks without upgrading, making the tool discoverable and reducing friction. The improved layout eliminates wasted whitespace.

2026-05-23
Billing & Access
Stabilised application slots and protected document downloads

Application assistance slots now use one consistent entitlement system, and tender document downloads are checked on the server before a download URL is issued.

Key Changes

  • Application assistance now consumes a subscription slot or purchased application credit atomically when a draft application is created.
  • Subscription usage displays now use the same slot counts across tender pages, dashboard cards, billing, and usage APIs.
  • Monthly application resets now reset slot usage instead of only resetting legacy compatibility counters.
  • Tender document download links now go through the authenticated platform resolver instead of exposing direct source links from the browser.

Platform Impact

Users see reliable application-slot counts, application creation no longer loses or double-counts credits, and document downloads follow the same access rules as the rest of the platform.

2026-05-23
User Dashboard
Fixed saved and export tender actions for logged-in users

Resolved an authentication mismatch on tender detail pages that could block logged-in users from saving tenders or exporting tender data.

Key Changes

  • Save and Export buttons now use the secure session cookie already used across authenticated client requests.
  • Logged-in users are no longer incorrectly asked to authenticate again when using tender quick actions.

Platform Impact

Users can reliably save tenders for later and export tender details from the tender sidebar without unnecessary login friction.

2026-05-23
AI Matching & Intelligence
DOCX and Word documents now processed through the AI analysis pipeline

The platform previously only extracted text from PDF tender documents. Word format documents (.docx, .doc) are now converted to text and analysed by the same AI pipeline, giving tenders with office-format attachments full AI enrichment.

Key Changes

  • Added DOCX text extraction support — Word documents are now converted and analysed alongside PDFs.
  • Legacy .doc files receive best-effort text extraction when DOCX conversion is not possible.
  • All office-format documents now get the same professional AI rewrite treatment as PDFs.

Platform Impact

Tenders published with Word-format attachments no longer miss AI analysis. More tenders receive summaries, key point extraction, and structured field data, improving matching and recommendation quality.

Related:/tenders/[id]
2026-05-23
AI Matching & Intelligence
AI document analysis rewrite now attempts extraction for every tender document

Removed the pre-filter that skipped AI analysis rewrite on documents classified as forms or cover pages. Every document now goes through the professional rewrite pass, and the AI quality gate handles low-substance output regardless of document type.

Key Changes

  • Removed the "has meaningful text" pre-filter that blocked many documents from the AI rewrite phase.
  • All tender documents now receive the full professional rewrite treatment, not just those classified as specification documents.
  • The AI quality gate continues to reject rewrite output that lacks substance — document type is no longer a factor.

Platform Impact

More tender documents receive professional AI analysis rewrites, increasing the coverage of readable supplier guidance across the platform.

Related:/tenders/[id]
2026-05-23
AI Matching & Intelligence
Fixed tender document extraction pipeline — R2 URLs now preferred over expired government sources

The document extraction pipeline was primarily using government eTenders download URLs for PDF extraction, which expire days after publication. All extraction paths now prefer Cloudflare R2 cached URLs first, ensuring extraction works reliably even when government URLs expire.

Key Changes

  • Backlog hydration (cron pipeline) now chooses R2 storage URLs before government download URLs.
  • Analysis trigger path now sends the correct document URL instead of undefined — unblocking extraction through the assist API.
  • Document processor worker now includes R2 URLs in its URL selection chain.

Platform Impact

Substantially more tender documents will be successfully extracted, especially older documents whose eTenders URLs have expired. R2-cached documents are now used as the primary extraction source.

2026-05-22
User Dashboard
Added bulk cleanup buttons to saved tenders drawer

The saved tenders sidebar drawer now includes "Remove Closed" and "Clear All" buttons so users can clean up non-actionable tenders without removing them one at a time.

Key Changes

  • Added "Remove Closed" button that deletes all saved tenders with CLOSED or AWARDED status in one click.
  • Added "Clear All" button that removes every saved tender from your list.
  • Both buttons show a confirmation dialog before any deletion occurs.
  • Buttons appear in the drawer footer when applicable and show a loading state while processing.

Platform Impact

Users can quickly clean up their saved tenders list — remove tenders that are no longer open for bidding, or start fresh with a single click.

2026-05-21
Billing & Access
PayPal subscription reliability fixes

Fixed multiple issues across the PayPal subscription lifecycle: auth header violations, missing promo code forwarding on resume, dead webhook code, polling on success page, and inconsistent downgrade protection.

Key Changes

  • Removed manual Authorization headers on pricing page — uses credentials:include only, aligning with the HTTP-Only cookie auth architecture.
  • Added polling (10 retries × 2s) to the payment success page so users see their subscription status even when the webhook arrives late.
  • Subscription resume now forwards the original promo code, preventing loss of promotional pricing.
  • The V1 cancel-subscription endpoint now calls PayPal API instead of just updating the local database.
  • Added downgrade protection to the update-subscription endpoint, consistent with the create path.
  • Removed dead CRC32 hash computation from webhook signature verification.
  • Added logger warnings for unknown PayPal webhook statuses.
  • Fixed bundled addon wallet persistance — added walletId field to AddonSubscription schema.

Platform Impact

More reliable subscription management: users see correct status on redirect, promo codes survive resume flows, cancellations actually cancel at PayPal, and operator visibility improves with better logging.

2026-05-21
Platform Updates
Removed discussion board from tender detail pages

The discussion board has been removed from all tender detail pages to simplify the user experience and streamline the tender viewing interface.

Key Changes

  • Removed DiscussionBoard component and its imports from all five tender detail layouts (Active, Awarded, Closed, Cancelled, Notice).
  • Removed DiscussionBoard from the SEO tender detail page.
  • Cleaned up unused DiscussionBoardSkeleton imports where present.

Platform Impact

Tender detail pages load slightly faster with less DOM overhead. Users can focus on tender content, documents, and submission requirements without the discussion board section.

2026-05-20
Platform Updates
Fixed "Am I Eligible?" crash, dashboard summary, and saved tenders drawer

Resolved three critical errors: the eligibility checker now works properly, the dashboard summary card no longer crashes, and the saved tenders sidebar overlay opens correctly instead of navigating to a broken page.

Key Changes

  • Fixed the "Am I Eligible?" tool on tender detail pages — the Next.js 15 route parameter was not being awaited, causing every check to fail with an error.
  • Fixed the dashboard summary API (500 error) by removing an invalid Prisma aggregate that caused the endpoint to crash on load.
  • Fixed the "View All" saved tenders button to open the existing saved tenders sidebar overlay instead of navigating to a non-existent page (404).

Platform Impact

Users can now check their eligibility on any tender without errors. The dashboard loads summary cards reliably again. Saved tenders are accessible via the side drawer overlay as intended.

2026-05-20
Platform Updates
Complete overhaul of Procurement Intelligence interface

Redesigned the Procurement Intelligence feed with a premium, high-tech aesthetic and introduced powerful new functional controls for sorting, searching, and filtering intelligence signals.

Key Changes

  • Introduced dynamic view modes (Detailed List, Compact List, and Grid View) for the intelligence feed.
  • Added instant client-side search to quickly filter intelligence items by keyword.
  • Added sorting controls to arrange signals by newest, oldest, or urgency level.
  • Upgraded intelligence cards with dynamic risk badges, animated urgency indicators, and hover effects.
  • Redesigned the intelligence dashboard layout with glowing statistical summaries and pill-shaped category tabs.

Platform Impact

Users can now quickly triage and explore critical government procurement signals with a highly responsive, customizable, and visually polished interface.

Related:/intelligence
2026-05-20
AI Matching & Intelligence
Stabilised "Am I Eligible?" checker with robust central intelligence service mapping

Overhauled the "Am I Eligible" tool to utilize the stable, production-tested TenderEligibilityService. This guarantees accurate matching for CIDB grades, B-BBEE levels, industry focus, and financial requirements, while resolving a critical profile-loading bug.

Key Changes

  • Refactored `/api/v1/tenders/[id]/eligibility-check` to call the robust TenderEligibilityService.
  • Fixed the CIDB profile loading bug by correctly joining the CompanyProfile relation during data retrieval.
  • Corrected the CIDB field reference from the typo-prone `cidbGrade` to the database column `cidbGrading`.
  • Improved BBBEE requirement checks to correctly scan the `minimumLevel` attribute from tender JSON data.
  • Integrated fuzzy, synonym, and semantic industry focus alignment checks using the central matcher service.
  • Seamlessly appended custom local province and tax compliance checks to preserve full client backward-compatibility.
  • Fully validated the changes under a clean compilation gate with zero TypeScript or ESLint errors.

Platform Impact

Users get 100% accurate, highly-detailed, and reliable eligibility checks instantly on any tender page, eliminating false-negative or buggy CIDB and B-BBEE matching errors.

2026-05-20
Security & Trust
Full secure-cookie migration: purged localStorage session dependencies and standardized Edge auth

A complete security hardening of the Tenders SA authentication system. The platform has fully transitioned to secure, HTTP-Only session cookies, eliminating all vulnerable client-side localStorage token tracking while maintaining backward compatibility and Edge runtime timings.

Key Changes

  • Purged all manual localStorage token reads, writes, and deletes across 93+ client components, pages, and hooks.
  • Configured standard fetch wrappers (authFetch, apiFetch, and DocumentApiClient) to systematically pass credentials with "include" for HTTP-Only session cookies.
  • Standardized and restored session cookies and JWT payloads to the canonical 7-day lifespan.
  • Introduced timingSafeEqualEdge constant-time XOR comparison to protect against timing attacks under the Next.js Edge runtime context.
  • Added middleware-level validation recovery that filters out invalid null or undefined tokens and automatically normalizes incoming headers.
  • Verified the complete codebase under a clean-compilation typecheck gate (npx tsc --noEmit).

Platform Impact

Eliminates potential token extraction vulnerabilities through XSS vectors by keeping JWT tokens strictly inside secure, HTTP-Only cookies. Standardizes session length to 7 days, significantly improving usability and authentication resilience.

2026-05-19
Company Intelligence
Major Forensic Analysis tool overhaul: bug fixes, improved detection, and platform-wide integration

The Forensic Analysis tool received a comprehensive overhaul fixing critical bugs, improving detection algorithms, and integrating forensic risk signals across the platform. Supplier risk scores now appear on tender detail pages, department scans show accurate per-department data, and Pro users can generate AI-powered audit narratives.

Key Changes

  • Fixed a critical bug where the deregistered supplier detection returned no results due to a missing database await.
  • Fixed the AI narrative API so Pro users can now generate forensic audit narratives with key findings and recommended actions.
  • Replaced unsafe SQL parameter patterns with Prisma parameterized queries across all detection functions.
  • Department scans now scope new-entrant detection to the selected department instead of showing global results.
  • Forensic risk scores now factor in recency — older flags contribute less to the overall score than recent ones.
  • Address clustering now normalizes addresses (expanding abbreviations like St → Street) so slight variations are detected as the same location.
  • Threshold proximity detection now runs a single consolidated query instead of five separate queries, improving performance.
  • Supplier forensic analysis pages now show flag prevalence data (Rare, Uncommon, Common) and related suppliers for each flag.
  • Awarded suppliers on tender detail pages now display a risk score badge linking to their full forensic analysis.
  • Department forensic pages now show dual Intelligence and Forensic action buttons for each top supplier.
  • Batch forensic score calculation reduces database queries by ~10x on department and tender detail pages.
  • Removed unused components and dead code to reduce maintenance burden.

Platform Impact

The Forensic Analysis tool now functions correctly, delivers more accurate detection results, and is integrated into the broader platform so users encounter risk signals naturally while browsing tenders and supplier profiles. Pro users gain AI-powered audit narratives.

2026-05-19
SEO & AI Visibility
AI Assistant visibility, dashboard improvements, and tender usability enhancements

A major usability update across the platform: the AI Assistant is now accessible from the navbar, keyboard shortcuts, and contextual pages. The dashboard gets summary cards, an activity feed, and proactive match alerts. Tender detail pages gain a quick summary and eligibility checker. Breadcrumb navigation is added to deep pages.

Key Changes

  • Added "Ask AI" gradient button in the header navbar for logged-in users and "Try AI" button for guests.
  • Added Sparkles icon shortcut in the desktop quick-access nav bar with active state when chat is open.
  • Added AI Assistant section to the hamburger menu dropdown with description text.
  • Added Ctrl+Shift+K (Cmd+Shift+K on Mac) keyboard shortcut to open AI chat from any page.
  • Added first-visit onboarding tooltip pointing users to the AI assistant.
  • Enhanced the floating chat bubble with Sparkles icon, hover tooltip, and first-visit pulse animation.
  • Wired up context-aware AI chat on tender detail pages, application pages, and the dashboard.
  • Added proactive match alert banner on the dashboard showing new matches since last visit.
  • Added 3-bullet Quick Summary card on tender detail pages for at-a-glance tender overview.
  • Added "Am I Eligible?" button on tender detail pages that checks CIDB, B-BBEE, province, and tax compliance against the user's company profile.
  • Added Dashboard Summary Cards: Deadlines, New Matches, Doc Alerts, and Pipeline Value.
  • Added Recent Activity feed on the dashboard showing application updates, new matches, and notifications.
  • Added breadcrumb navigation to tender detail pages and all dashboard pages.

Platform Impact

Users can discover and access the AI Assistant from multiple touchpoints. The dashboard provides at-a-glance triage of urgent items. Tender pages give instant eligibility and summary information. Breadcrumbs improve navigation context on deep pages.

2026-05-18
Platform Updates
Fixed AI enrichment cron crash and refactored Cloudflare client to native REST API

The AI enrichment cron was silently crashing due to invalid enum values in the tender query. The Cloudflare Workers AI client now uses the native REST API directly instead of the OpenAI compatibility layer.

Key Changes

  • Fixed aiEnrichmentStatus query using invalid values null and "PENDING" — changed to valid enum value "NOT_PROCESSED" so unprocessed and failed tenders are actually picked up.
  • Refactored Cloudflare client from OpenAI SDK to native Cloudflare Workers AI REST API at /ai/run/{model}, removing a dependency and aligning with Cloudflare documentation.

Platform Impact

AI enrichment cron now correctly processes unprocessed and previously-failed tenders instead of crashing on every run with a Prisma validation error.

2026-05-18
Performance & Reliability
Added Cloudflare Workers AI provider with high-context models

Cloudflare Workers AI is now the first fallback in the AI provider chain, bringing large-context models (262K tokens) for better document reading and reducing reliance on third-party APIs.

Key Changes

  • Added Cloudflare Workers AI provider as the primary fallback in the AI provider chain.
  • High-context models kimi-k2.6 (262K) and kimi-k2.5 (256K) allow reading full tender documents without truncation.
  • Fallback chain includes gpt-oss-120b, Llama 3.3 70B, Llama 4 Scout, and Llama 3.1 8B for reliability.

Platform Impact

AI enrichment is more resilient with an additional provider at the top of the chain. Large documents benefit from 262K context windows.

2026-05-18
AI Matching & Intelligence
Improved AI rewrite quality, fixed duplicate document sync, added NVIDIA AI provider

The AI analysis rewrite now correctly uses already-extracted data instead of discarding it. Documents with duplicate filenames are no longer skipped during sync. A new NVIDIA AI provider gives more reliable model fallback.

Key Changes

  • AI rewrite prompts now treat the existing extraction data as the primary source — models refine what is already there instead of returning null sections and discarding populated fields.
  • Quality gate no longer rejects rewrites that preserve existing extraction data, and no longer lowers stored confidence on each failed pass.
  • Document sync no longer skips both documents when two have identical filenames — both are saved and available for download.
  • Added NVIDIA AI provider (mistralai/mistral-medium-3.5-128b and minimaxai/minimax-m2.7) as the first fallback option in the AI provider chain.

Platform Impact

More tender analyses will pass the quality gate and show enriched content. Documents with duplicate names are preserved instead of lost. The AI fallback chain is more resilient with an additional provider.

2026-05-18
AI Matching & Intelligence
Fixed AI enrichment crash and retried previously failed tenders

Fixed a crash where the AI enrichment cron failed when the AI model returned key requirements as an array instead of a string. Previously failed tenders will now be automatically retried.

Key Changes

  • Fixed "b.aiKeyRequirements?.trim is not a function" crash — the parser now handles array responses from the AI model by joining them into a string instead of crashing.
  • The AI summary cron now retries tenders that previously failed (status FAILED), not just unprocessed ones — 176 previously stuck tenders will be picked up on the next run.
  • Made the rewrite pipeline's asStringOrNull helper handle arrays too, preventing silent data loss when the AI returns non-string formats.

Platform Impact

AI enrichment is more robust against variable AI response formats. Previously failed tenders will be automatically retried and enriched on the next cron cycle.

2026-05-18
User Dashboard
Daily Tender Digest now sent to all users with matching improvement tips

The daily digest email now reaches every user with a company profile, not just those with qualifying tender matches. A new evergreen section explains how to improve matching accuracy.

Key Changes

  • Every user with a company profile now receives a daily digest email — no more silent skipping when there are no qualifying matches.
  • Added a permanent "Improve Your Matching Accuracy" section with 5 actionable tips: complete your company profile, update B-BBEE level, set operating provinces, add project experience, and review matches daily.
  • Subject line adapts based on content: shows match count when matches exist, or "Improve Your Matching Profile" for users without active matches.

Platform Impact

Users who previously received no digest (because they only had potential or near-miss matches) now get daily guidance and matching tips. The evergreen tips section helps all users improve their match quality over time.

2026-05-17
AI Matching & Intelligence
Fixed broken intelligence sources and added AI-powered relevance filtering

Replaced broken government source feeds with working HTML scrapers and introduced an LLM-based relevance filter to replace the keyword title filter that was silently dropping all intelligence items.

Key Changes

  • Fixed CIDB Press Room feed: broken RSS URL (returned HTML, not XML) replaced with direct HTML scraping of the Press Room page.
  • Fixed AGSA Media Releases: corrected 404 URL to /MediaRoom/MediaStatements.aspx and removed Playwright fallback that silently failed in production.
  • Fixed Parliament Press Releases: corrected 404 URL to homepage scrape with fallback text scanning.
  • Fixed OCPO date parsing: invalid SharePoint dates no longer crash Prisma create.
  • Replaced keyword title filter (killed 59 of 63 items per run) with LLM-based relevance filter using Gemini.
  • Added LLM-based intelligence extraction: each relevant article is analyzed for key points, risk flags, affected organizations, urgency, and category.
  • Updated intelligence feed to sort by relevance score and include source names.
  • Added risk flags, key points, and urgency icons to the intelligence feed, widget, and assistant research panel.
  • Fixed proactive assistant intel query: corrected field mapping (headline, externalUrl, keyPoints) and added classificationStatus filter.

Platform Impact

Procurement intelligence pipeline now actually delivers classified, decision-relevant news items instead of silently producing zero output. Risk flags (budget cuts, audit failures, spending halts) surface directly on tender detail pages and the assistant dashboard.

2026-05-17
Performance & Reliability
Automated backfill cron for older documents awaiting AI rewrite

A new cron route scans existing tender analyses that only have regex extraction and enqueues them for AI professional rewrite, ensuring previously ingested documents are not left in the "still being refined" state indefinitely.

Key Changes

  • Added /api/cron/analysis-backfill which scans for aiExtractionMethod != "ai_rewrite" records and adds them to the enhancement queue.
  • Suitable for scheduling via crontab — lightweight DB scan + Redis enqueue; actual LLM processing is throttled by the enhancement queue processor.

Platform Impact

Older documents that were ingested before the mandatory rewrite was introduced will no longer be permanently stuck in regex-only state.

2026-05-17
AI Matching & Intelligence
AI professional rewrite now runs immediately on every ingested document

The AI analysis rewrite phase is now a mandatory, synchronous step in document ingestion. Every extracted tender document is immediately transformed into professional, supplier-friendly guidance rather than waiting for a background cron job that may never trigger.

Key Changes

  • TenderAnalysisRewriteService is called synchronously after every successful extraction — the rewrite is no longer an optional background task.
  • Added a background enhancement queue processor with automatic polling every 60 seconds to catch any backlogged or failed rewrites.
  • Unified service orchestration so the enhancement queue starts alongside the extraction queue on server boot.
  • If the immediate rewrite fails (e.g. LLM provider timeout), it falls back to the Redis-backed enhancement queue for retry with exponential backoff and a Dead Letter Queue after 3 failures.

Platform Impact

All tender documents now receive enriched summaries, key requirements, and SEO-optimized metadata immediately upon ingestion. The "Analysis is still being refined" badge resolves to completed by the time a user first views the tender.

2026-05-16
Platform Updates
Preparation Planner now shows Closed status instead of negative days

The Tender Preparation Planner no longer displays negative remaining days for closed tenders. It now correctly identifies past-closing-date tenders and shows them as Closed.

Key Changes

  • Added CLOSED urgency state to the Preparation Planner timeline calculation
  • Detects when closing date has passed and displays "Closed" instead of negative working day counts
  • Hides scheduling actions (Add Event, Auto, AI Schedule) for closed tenders
  • Shows clear "This tender has closed" messaging with a lock icon

Platform Impact

Users viewing closed tenders will no longer see confusing negative day counts — the planner correctly identifies the tender is closed.

2026-05-16
Company Intelligence
Cleaned up company names in Likely Bidders list

Sanitized company names in the application assistance research tab to remove leading/trailing dashes, brackets, and special characters.

Key Changes

  • Applied company name sanitization to Likely Bidders list in the research tab of the application assistance dashboard
  • Removes extraneous dashes, parentheses, and punctuation from the start and end of company names

Platform Impact

Company names in the Likely Bidders card now display cleanly without formatting artifacts.

2026-05-16
User Dashboard
Fixed Match Score display crash and separated AI recommendations

Fixed a React page crash on the dashboard matches page caused by incompatible data formats in improvement areas. AI recommendations are now stored in a dedicated model separate from matching scores for better reliability.

Key Changes

  • Fixed React error #31 crash on /dashboard/matches page when rendering match improvement tips
  • Created dedicated AiRecommendation model to separate AI-generated recommendations from algorithmic matching scores
  • Normalized improvement areas format to consistently return string arrays regardless of backend pipeline
  • Improved type safety across recommendation worker, recommendation service, and dashboard UI

Platform Impact

The dashboard matches page no longer crashes for users with active matching scores. AI recommendations are more reliably displayed.

2026-05-15
Security & Trust
Improved authenticated session reliability

Strengthened authenticated API request handling so valid signed-in sessions are recognized more consistently across dashboard and admin workflows.

Key Changes

  • Improved server-side handling for secure cookie-based sessions.
  • Kept existing bearer-token API compatibility for current integrations and admin tools.

Platform Impact

Signed-in users should see fewer inconsistent authorization failures when their secure session cookie is valid.

2026-05-15
Platform Updates
Improved tender detail navigation

Added clearer navigation between internal tender detail pages and public tender pages, and restored organization links from public tender sidebars.

Key Changes

  • Added a Public Tender Page quick action on internal tender detail pages.
  • Restored clickable issuing organization links in public tender detail sidebars.

Platform Impact

Users can move between tender detail views and the issuing organization page without losing context.

2026-05-15
AI Matching & Intelligence
Restored full tender matching runs

Fixed admin and scheduled full matching triggers so full recalculations rebuild tender-company matches instead of silently skipping when incremental state has no detected changes.

Key Changes

  • Updated full matching cron behavior to run a full refresh by default.
  • Updated admin matching controls so full recalculation requests explicitly refresh all active tender-company matches.

Platform Impact

New and existing opportunities can be matched reliably again from scheduled automation and the admin dashboard.

2026-05-14
Platform Updates
Improved feature page stats readability

Updated the live stats section on the Features page so key platform metrics are easier to read against the green background.

Key Changes

  • Increased contrast for live stats cards on the Features page.
  • Improved card spacing and fixed card height so the stats section scans cleanly on mobile and desktop.

Platform Impact

Visitors can read platform proof points more easily when evaluating Tenders SA features.

Related:/features
2026-05-14
Tender Data
Fixed tender province mismatches on detail pages

Fixed a data quality issue where tenders displayed incorrect provinces in the hero section and structured data. The system now resolves the province from the delivery address, which reflects the actual work location rather than the issuing department's registered address.

Key Changes

  • Fixed OCDS sync to not overwrite province when delivery address suggests a different province.
  • Added displayProvince computation at page render time, prioritizing delivery address over stored province.
  • Updated all tender detail layouts (active, awarded, closed, cancelled, notice, expired) to use the resolved province.
  • Updated JSON-LD structured data, metadata, and schema markup to use the resolved province for SEO consistency.

Platform Impact

Tenders now display the correct province based on delivery/work location rather than the issuing department's registered address. Structured data and SEO metadata are consistent with the visible hero badge.

2026-05-14
Company Intelligence
Cleaned company names on intelligence leaderboards

Improved company-name cleanup across Company Intelligence leaderboards and award search results so supplier names display cleanly and link to the right profiles.

Key Changes

  • Removed leading and trailing dash wrappers from displayed company names.
  • Applied the same company-name cleanup before generating profile links from leaderboard and award result entries.
  • Kept bracketed supplier-name cleanup consistent across widgets, search results, and provincial leaderboard listings.

Platform Impact

Users see cleaner supplier names and can open the intended Company Intelligence profile even when source award data contains stray brackets or dashes.

2026-05-14
Security & Trust
Updated public trust and document availability messaging

Refined public trust copy so privacy, operator identity, social proof, and tender document availability are stated more accurately.

Key Changes

  • Updated homepage and contact privacy wording to describe POPIA-aligned practices without unsupported certification-style claims.
  • Added registered operator details to the contact page for clearer platform legitimacy.
  • Removed unsupported static publisher impression and verified publisher counts from public marketing.
  • Removed unsupported static results counters from the How It Works page.
  • Replaced pricing-page testimonials with benefit-focused platform guidance until verified customer reviews are available.
  • Changed the features page registered account count to use verified live platform data.
  • Updated tender detail pages so document actions only appear when documents are actually attached.

Platform Impact

Visitors now see clearer, evidence-aligned trust messaging and tender document availability cues before deciding how to proceed.

2026-05-14
Platform Updates
Added Changelog link to main navigation

Added a prominent Changelog link to the main site navigation so users can easily find platform update history.

Key Changes

  • Added Changelog entry to the hamburger menu under a new "Updates" section with amber-colored History icon, separated from main Menu items by a divider.
  • Added Changelog shortcut button to the desktop quick-access nav bar, separated from Pricing by a vertical divider.
  • Added Changelog to the header navigation links array for SEO consistency.

Platform Impact

Users can now discover and navigate to the platform changelog directly from any page on the site.

Related:/changelog
2026-05-14
Security & Trust
Cleaned up public trust and pricing claims

Updated public marketing copy so trust badges, pricing, and social-proof statements stay aligned with verified platform information.

Key Changes

  • Replaced the homepage trust claim block with the approved Trust Center badge display.
  • Corrected public Starter pricing references from R199 to R499 where stale pricing appeared.
  • Removed static business and user-count claims from public marketing, auth, blog, and promotional pages where no verified live count was available.

Platform Impact

Visitors now see more accurate public claims about Tenders SA without inflated user counts or unsupported trust messaging.

2026-05-14
Company Intelligence
Added weekly and monthly time-window award pages

New dedicated pages for browsing tender awards from the last 7 days or last 30 days, plus award freshness metrics in the system health check.

Key Changes

  • Created weekly award page at /tools/company-intelligence/awards/weekly showing awards from the past 7 days.
  • Created monthly award page at /tools/company-intelligence/awards/monthly showing awards from the past 30 days.
  • Added pill navigation between Latest, Weekly, and Monthly award views on the Company Intelligence landing page.
  • Added award count and newest award age metrics to the ingestion health check script with WARN at 48h and FAIL at 72h thresholds.
  • Added shared getTimeWindowAwardFeed() data-access function for moving time-window award queries.

Platform Impact

Users can now discover recent award activity by time window and operators can detect award pipeline stalls earlier.

2026-05-14
AI Matching & Intelligence
Hardened AI tender analysis rewriting across providers

Improved the tender analysis rewrite pipeline so multiple AI providers receive a stricter role, input, output, and quality contract, with more resilient parsing of provider responses.

Key Changes

  • Expanded the rewrite prompt with a clear procurement analyst role, source hierarchy, section instructions, output schema, and confidence calibration.
  • Made rewrite parsing tolerant of common provider formatting drift such as code fences, surrounding prose, trailing commas, wrapped result objects, and comma-separated keyword strings.
  • Reduced unnecessary quality rejections caused by repeated missing-information wording while keeping unsupported fact checks in place.

Platform Impact

Tender document analysis rewrites are more likely to save useful, grounded supplier guidance while rejecting risky or unsupported AI output.

2026-05-14
AI Matching & Intelligence
Added quality-gated AI rewriting for tender document analysis

Introduced a post-extraction refinement stage that rewrites tender document analysis into clearer supplier guidance only when it passes accuracy and quality checks.

Key Changes

  • Added a quality gate so low-confidence AI rewrites do not overwrite existing tender document analysis.
  • Updated the analysis enhancement queue to use the shared AI provider fallback system instead of a direct provider-specific path.
  • Added tender-level AI and SEO metadata updates from accepted rewritten analysis.
  • Added a backfill script for safely queueing or processing older document analysis records.
  • Added a refinement status on tender analysis cards when raw extraction is still awaiting AI cleanup.

Platform Impact

Tender detail pages can show clearer document guidance while preserving original tender facts and rejecting risky AI output.

2026-05-14
AI Matching & Intelligence
Refactored Procurement Intelligence to official news sources only

Rebuilt the procurement intelligence scraper contract so it runs hourly against official government procurement news sources and no longer ingests tender opportunity data.

Key Changes

  • Changed the intelligence scraper cron mirror from twice daily to hourly.
  • Removed eTenders, tender bulletin, CIDB register, and provincial tender opportunity sources from the procurement intelligence scraper registry.
  • Added a hard source policy allowing only official procurement news sources such as Gov.za, OCPO, SAnews, CIDB, Parliament, and AGSA.
  • Forced new intelligence items to stay unpublished until classified and filtered public feeds to active classified items only.
  • Repaired source health to use IntelSource and IntelItem data without the invalid per-source ScraperRun contract.
  • Connected admin publication to intelligence alert matching and added an hourly alert delivery cron.

Platform Impact

Procurement Intelligence now focuses on decision-support news, policy, audit, regulatory, and enforcement updates instead of tender listings.

2026-05-14
Procurement Compliance
Launched Legislation & Compliance Portal with expert content

Introduced a dedicated legislation portal at /legislation with expert analysis, compliance checklists, and sector-specific guides for South African procurement law.

Key Changes

  • Added expert summaries, key provisions, and compliance checklists to all 10 SA procurement law entries (PFMA, MFMA, PPPFA, CIDB, B-BBEE, POPIA, PSIRA, NHA, Constitution S217, PPA 2024).
  • Added SITA Act legislation entry for ICT procurement compliance.
  • Created legislation landing page with search, sector grid, and full legislation index.
  • Created legislation detail pages (/legislation/acts/[slug]) with expert summary, key provisions, compliance checklists, metadata sidebar, and related sector links.
  • Created sector compliance pages (/legislation/sectors/[slug]) for Construction, ICT, Security, Professional Services, and Healthcare with compliance roadmaps, accreditations, pitfalls, and related blog article links.
  • Added legislation navigation links in header, hamburger menu, and footer.
  • Cross-linked legislation portal from tender detail pages, glossary page, and compliance checker tool for SEO.

Platform Impact

Users can now research procurement legislation with expert-written guidance, check sector-specific compliance requirements, and navigate between laws and related blog articles from a central portal.

2026-05-14
Company Intelligence
Sanitised company names in Market Intelligence widget

Fixed the Market Intelligence sidebar widget on tender detail pages so bracketed company names are sanitised for both display and slug generation.

Key Changes

  • Replaced inline slug generation with canonical `generateCompanySlug` which strips brackets and trims leading/trailing hyphens.
  • Sanitised displayed company names with `sanitizeCompanyName` so brackets like (Company Name) render as Company Name.
  • Links now correctly resolve to the intended company intelligence page even when the raw supplier name starts or ends with parentheses.

Platform Impact

Company names with brackets in the Market Intelligence widget now display cleanly and link to the correct company intelligence page.

2026-05-14
Billing & Access
Fixed discounted subscription retry after PayPal cancellation

Users who cancelled a discounted subscription mid-flow on PayPal can now retry without seeing an error screen.

Key Changes

  • Added canRetryPromotion check to allow promo re-use when the previous subscription never reached ACTIVE or TRIAL status.
  • Old promotion redemption records are cleaned up when the associated subscription was cancelled before PayPal approval.

Platform Impact

Free and Trial users who cancel on PayPal can now retry the discounted subscription without being blocked by a stale redemption record.

2026-05-13
Performance & Reliability
Simplified document extraction queue handoff

Updated the document extraction queue so it hands queued document URLs directly to the extractor service instead of rejecting them based on URL shape.

Key Changes

  • Removed late-stage URL trust checks from the extraction queue path.
  • Added bounded hydration so pending document rows can be restored into the Redis extraction queue before cron sweeps.
  • Expanded queue health reporting to show pending document backlog that is queueable but not yet processed.
  • Aligned the extractor service default URL policy with platform and eTenders document sources.
  • Kept extraction results and failure reasons as the source of truth for processing outcomes.
  • Clarified that R2 cache metadata is still only set from confirmed Worker/R2 state.

Platform Impact

Document processing failures now surface from the extractor itself, making upstream ingestion issues easier to diagnose while allowing more documents to be processed.

2026-05-13
Performance & Reliability
Added production SSH health reporting

Introduced a read-only operational health report for monitoring core platform dependencies from the AWS app host.

Key Changes

  • Added checks for application health, Worker availability, tender ingestion freshness, document availability, cron logs, Nginx anomalies, app errors, and queues.
  • Added JSON and Markdown report outputs that can later feed the admin dashboard.
  • Corrected the tender ingestion helper so the newest tender age metric is reported instead of returning an unknown check result.

Platform Impact

Operators can diagnose production reliability issues faster before exposing the same signals in the dashboard.

2026-05-10
SEO & AI Visibility
Improved tender document visibility for search and AI discovery

Tender detail pages now expose official document links through stable platform-controlled URLs in structured data.

Key Changes

  • Updated document JSON-LD to use real Worker/R2 document URLs.
  • Removed non-existent internal document URLs from tender document schema.

Platform Impact

Users, search engines, and AI assistants can better verify tender documents from public tender pages.

2026-05-10
Procurement Compliance
Added procurement legislation context to tender pages

Tender pages now show relevant South African procurement rules in a visible compliance context section.

Key Changes

  • Added visible procurement legislation references for public tenders.
  • Connected the visible legislation section to the same citation data used by JSON-LD schema.

Platform Impact

Suppliers get clearer procurement context while search engines receive consistent structured data.

2026-05-09
AI Matching & Intelligence
Enhanced tender recommendation precision

Improved the AI matching algorithm to better account for B-BBEE levels and CIDB grading requirements.

Key Changes

  • Weighted B-BBEE compliance more heavily in preference-based scoring.
  • Added strict CIDB grade filtering for construction sector tenders.

Platform Impact

Users receive more accurate and compliant tender recommendations tailored to their company profiles.

2026-05-08
AI Matching & Intelligence
AI-Powered eligibility and requirement deduction

Tenders now use AI to deduce CIDB grading, B-BBEE levels, and turnover requirements, even when not explicitly stated in the source data.

Key Changes

  • Added matching-specific AI enrichment for active tenders.
  • Improved deduction of CIDB levels based on tender value and category.
  • Enhanced B-BBEE requirement estimation for more accurate matching.

Platform Impact

Users receive matches for tenders that previously lacked structured eligibility data, increasing opportunity discovery by ~40%.

2026-05-08
User Dashboard
Complete overhaul of Application Assistance

A new 6-tab guided workflow helps users go from tender discovery to a full submission package with AI-generated documents.

Key Changes

  • Launched a 6-tab interface: Overview, Proposal, Documents, Checklist, Status, and Submission.
  • Integrated full Company Profile data into AI generation context.
  • Added PDF and DOCX export for complete tender proposals.
  • Introduced contextual tips and quality checks for bid preparation.

Platform Impact

Reduces the time to prepare a professional tender submission from days to minutes.

2026-05-07
Company Intelligence
New Tender Award & Company Intelligence Tool

Search and analyze the award history of any South African company or government organization.

Key Changes

  • Launched search for tender awards by company name or reference number.
  • Created detailed company award report pages with historical performance.
  • Integrated credit-based report unlocking for deep bidder analysis.

Platform Impact

Suppliers can research competitors and buyers can verify bidder track records with reliable data.

2026-05-06
Document Downloads
Stabilized tender document storage and retrieval

Migrated all tender documents to a platform-controlled Cloudflare R2 storage system to prevent broken links from government portals.

Key Changes

  • Implemented a unified Worker-based ingestion system for all tender documents.
  • Automated repair of broken source URLs and duplicated file extensions.
  • Serving documents via docs.tenders-sa.org for consistent user access.

Platform Impact

Users no longer face "404 Not Found" errors when trying to download critical tender documents from government sites.

2026-05-04
Company Intelligence
Launched Forensic Intelligence Platform

A first-of-its-kind tool for detecting procurement risks, supplier concentration, and anomaly patterns in public spending.

Key Changes

  • Developed real-time risk scoring for suppliers based on award history.
  • Implemented department-wide concentration analysis to identify dominant bidders.
  • Added anomaly detection for unusual award patterns and pricing outliers.
  • Integrated forensic teasers on company profile pages.

Platform Impact

Users can perform deep due diligence and identify potential procurement irregularities before they become liabilities.

2026-05-01
AI Matching & Intelligence
AI-Powered Tender Q&A Chat

Deep bid assistance that answers questions grounded specifically in the tender documents and your company profile.

Key Changes

  • Launched a conversational AI panel inside the Application Assistance workspace.
  • Grounded AI responses in extracted tender document text for high accuracy.
  • Automatically cites sections of the tender document in its answers.
  • Pre-loaded with company-specific context (B-BBEE, CIDB, Experience).

Platform Impact

Suppliers can instantly find specific requirements (like "What is the insurance requirement?") without reading hundreds of pages manually.

2026-04-28
Procurement Compliance
Automated CIDB & B-BBEE Compliance Calculators

New self-service tools to help businesses calculate their official CIDB grading and B-BBEE levels.

Key Changes

  • Implemented official CIDB Table A (Contract Value) and Table C (Financial) logic.
  • Added B-BBEE scorecard estimation based on latest sector codes.
  • Integrated results directly into user company profiles for one-click matching.
  • Provided "Gap Analysis" reports showing exactly what is needed for the next grade.

Platform Impact

Suppliers no longer need to guess their eligibility; they can now verify it instantly against official rules.

2026-04-26
Performance & Reliability
Platform Reliability Milestone

A massive engineering effort to stabilize the platform architecture and ensure zero-downtime performance.

Key Changes

  • Achieved a zero-error codebase across 83 core modules (TypeScript & Lint).
  • Implemented 300+ new automated unit tests covering critical path logic.
  • Launched enterprise-grade resilience patterns: Circuit Breakers and Distributed Locking.
  • Optimized connection pooling and memory management for high-traffic stability.

Platform Impact

Users experience a faster, more reliable platform with significantly reduced technical debt and fewer runtime errors.

2026-04-20
User Dashboard
Launched Tender Preparation Planner

A project management tool specifically designed for the complexities of South African tender submissions.

Key Changes

  • Automated milestone generation based on tender closing dates.
  • Added document checklist tracking with built-in compliance templates.
  • Implemented team task assignment and progress monitoring.
  • Added "Readiness Score" to evaluate bid quality before submission.

Platform Impact

Suppliers can now manage their entire bid preparation process in one place, reducing the risk of missing deadlines or documents.

2026-04-15
Search & Filtering
Interactive Provincial Tender Heatmap

Visual market density analysis allowing users to identify South African tender hotspots by province and industry.

Key Changes

  • Developed interactive D3-powered map of South Africa with real-time data overlays.
  • Added industry-specific density filters (e.g., Construction in Gauteng vs. Health in Western Cape).
  • Implemented "Province Health" metrics showing market competitiveness and value.
  • Added exportable provincial market reports for business development.

Platform Impact

Businesses can now visually identify where their services are most in demand across the country.

2026-04-10
Search & Filtering
Smart RSS Feeds for Tender Notifications

A new way to stay updated without checking your inbox. Tenders can now be delivered directly to your preferred news reader.

Key Changes

  • Launched provincial and category-specific RSS feeds.
  • Added "Smart RSS" guides for Mobile (Feedly, Reeder) and Desktop (NetNewsWire).
  • Integrated RSS discovery into the "How it Works" section.
  • Added one-click RSS subscription for pro users.

Platform Impact

Power users can now integrate tender alerts into their existing news and intelligence workflows.

2026-04-05
AI Matching & Intelligence
24/7 AI Procurement Assistant

A platform-wide AI support system trained on the full Tenders SA knowledge base and South African procurement law.

Key Changes

  • Indexed 135+ expert procurement guides and legislation docs for AI context.
  • Enabled real-time Q&A on any platform page via the persistent chat bubble.
  • Integrated "Human-in-the-loop" support ticketing for complex regulatory queries.
  • Added streaming responses for a faster, conversational experience.

Platform Impact

Users get instant, grounded answers to complex procurement questions without leaving the platform.

Related:/
2026-04-02
Search & Filtering
Unified Blog Search

A fast, intelligent search engine that covers all 135+ procurement guides and blog posts.

Key Changes

  • Implemented full-text search across both Markdown and Database blog posts.
  • Added real-time search debouncing for instant feedback.
  • Enhanced search scope to include titles, excerpts, tags, and categories.
  • Improved search UI in the blog hero section.

Platform Impact

Users can find specific procurement advice or legislation guides in seconds.

Related:/blog
2026-03-30
Procurement Compliance
Joint Venture (JV) Suite for Partnerships

Tools to help smaller businesses partner up for larger tenders through automated JV grading and agreement drafting.

Key Changes

  • Implemented the official CIDB Joint Venture grading calculator.
  • Added automated share-of-work and profit calculation tools.
  • Created a JV Agreement generator pre-filled with partner and tender data.
  • Integrated B-BBEE weighted average calculations for partnerships.

Platform Impact

Empowers smaller contractors to take on larger projects by simplifying the legal and technical complexity of JVs.

2026-03-24
SEO & AI Visibility
Major SEO & Structured Data Upgrade

Comprehensive upgrade of platform schema markup to improve visibility in Google Rich Results and AI discovery.

Key Changes

  • Implemented FAQ and HowTo schema for all blog guides.
  • Added specialized JobPosting and Document schema for tender detail pages.
  • Corrected BreadcrumbList structures for Google Search Console compliance.
  • Added geographic targeting metadata for provincial tender categories.

Platform Impact

Higher visibility in search engines and better comprehension of tender data by AI assistants like Gemini and ChatGPT.

2026-03-20
AI Matching & Intelligence
AI Tender Value Estimator

A predictive tool that estimates the likely value of a tender based on 5 years of historical award data.

Key Changes

  • Developed a statistical estimator using OCDS award data from 2021-2026.
  • Added value range predictions (Low, Median, High) for active tenders.
  • Integrated "Comparable Tenders" display to show similar historical awards.
  • Linked the estimator directly into the Application Assistance pricing module.

Platform Impact

Suppliers can bid more competitively by understanding the historical price points for similar work.

2026-03-16
Tender Data
New National & Provincial Organization Directory

Launched a comprehensive directory of South African procuring organizations, grouped by type and region.

Key Changes

  • Created profile pages for SOEs, municipalities, and national departments.
  • Added tabbed tender listings for each organization (active, awarded, closed).
  • Implemented automatic organization enrichment from historical award data.

Platform Impact

Suppliers can now research a specific buyer’s entire procurement history and current opportunities in one place.

2026-03-10
Platform Updates
Automated Weekly Blog Digest

A new automated newsletter that delivers the most relevant procurement guides and news to your inbox every week.

Key Changes

  • Implemented a weekly cron job to aggregate top-performing blog content.
  • Created a dynamic email template for the Weekly Digest.
  • Added automated segmenting to send province-specific news to relevant users.
  • Integrated one-click unsubscribe and preference management.

Platform Impact

Users stay informed about critical procurement changes without having to manually check the site.

2026-03-05
SEO & AI Visibility
Publisher Showcase & Partner Badges

A new programme for publishers who use Tenders SA data, featuring a showcase gallery and "Verified Partner" badges.

Key Changes

  • Launched the Publisher Showcase gallery at /publishers.
  • Created "Powered by Tenders SA" badges for external site integration.
  • Implemented a submission form for publishers to join the showcase.
  • Added do-follow link attribution for verified partners.

Platform Impact

Increases platform authority and creates a network of high-quality backlinks from procurement-related sites.

Related:/publishers
2026-03-01
User Dashboard
New User Onboarding & Activation Experience

A guided experience for new users to help them set up their company profile and receive their first tender matches.

Key Changes

  • Launched an interactive onboarding checklist for new signups.
  • Added progress-based "Unlock" rewards for completing profile sections.
  • Implemented automated welcome emails with personalized tender tips.
  • Created a "First Match" walkthrough to explain scoring and requirements.

Platform Impact

New users reach value faster, increasing profile completeness by 55% within the first 24 hours.

Related:/dashboard

Questions about these updates?

Our team is dedicated to building the best procurement technology for South Africa. If you have feedback or questions about any change, please reach out.

Contact Support