Skip to main content
Tender approval and private tender links both live in stralos-api-estimate (migration 0114_tender_approvals_links.sql, schema in src/db/tenderApprovalSchema.ts). The company setting that shows the approval UI, tendersNeedApproval, is in api-global’s organisation settings; api-estimate never reads it. For the estimator’s view, see Tender approval and Share a link.

Tender approval

Approval is a record only. The tender stays a draft, nothing here changes its status, and Submit never reads it.
  • A request belongs to a revision: a draft’s is the one it will become. A revised tender starts with no approval.
  • One waiting request per estimate, enforced by the partial unique index estimate_tender_approvals_one_waiting. Sending a new request withdraws the waiting one in the same transaction.
  • A decision or withdrawal is written once, by an UPDATE guarded on status = 'waiting'. CHECK constraints tie the decision and withdrawal columns to the status.
  • The approver is a business person, never a contractor. Before the row is written, api-estimate asks api-global to put them on the project (POST /api/global/service/organisations/:orgId/projects/:projectId/approvers), forwarding the estimator’s own token, so api-global decides who may be added.

Routes

All under /api/estimate (src/routes/tenderApprovalRoutes.ts): Notes are trimmed and at most 1,000 characters. Each approval carries its status (waiting, approved, sent_back, withdrawn), the names of the requester, approver and withdrawer, the notes, and three prices excluding GST: priceAtRequest, priceAtDecision, and currentPrice (a submitted revision’s frozen price, otherwise the live total). figuresChangedSince compares the current price to the cent with the price at request (waiting) or at decision (approved), and is false otherwise. canDecide is true for the approver of a waiting request, and canWithdraw for its requester or an admin. Requests, decisions, and withdrawals enqueue tender_approval_requested, tender_approval_decided, and tender_approval_withdrawn in the row’s transaction; they are sent after it commits. See Notifications. A private link shows one recipient the tender as a web page with no sign-in. It is api-estimate’s first unauthenticated surface over tenant data, so the public half is deliberately narrow (src/routes/tenderLinkRoutes.ts):
  • The token is 32 random bytes and only its SHA-256 is stored (estimate_tender_links_token_hash_unique). The URL is in the create response only.
  • A malformed or unknown token is a 404 before any tenant read. A known hash is looked up on the platform pool in one narrow query; everything after runs on the RLS runtime pool confined to the link’s organisation and project (withProjectTenant).
  • The public routes write only the view log, the first-open stamp, and its notification: no save, no D&C sync.
  • The auth exemption in app.ts names exactly the three public GET routes, never a prefix.
  • Responses carry Cache-Control: no-store, X-Robots-Tag: noindex, nofollow, noarchive, Referrer-Policy: no-referrer, X-Content-Type-Options: nosniff, X-Frame-Options: DENY, and a CSP with no script. No estimate id appears on the page.

What the page shows

A link belongs to the revision it was made on (a draft’s is the one it will become). The page is drawn on every request from that revision’s figures now: the draft while it is being priced, the estimate while it is frozen at that revision, then that revision’s saved version once the tender is revised. Once a newer revision is submitted, the page says the tender has been updated and gives the email of whoever submitted it. A link works until the tender’s validity ends: the end of the local day 30 days after the tender’s date. With prices left out, the page shows the schedule with quantities only, and the file routes answer 404.

Routes

Member routes, scoped like every estimate route: Public routes:

States and limits

Per link, 60 page views and 30 file opens an hour, counted from the view log (429 with Retry-After). Unknown tokens are limited to 30 misses per network per 10 minutes on each instance. Each view records when, what was opened (page, pdf, or xlsx), the outcome, a rough browser name, and the network as an IPv4 /24 or IPv6 /48, never a full address. The first view that shows the tender stamps the link and enqueues tender_link_first_opened for the person who made it. The link row also keeps a snapshot of the names and branding it was made with (display, logo). Approvals and links are not copied when an estimate is copied.