Saltar al contenido principal

Legal Documents and Consent

How the Terms, Privacy, Cookie and Refund policies are published, how visitors and users accept them, and what the code records as proof.

The policy texts are drafts written for Guatemala (Constitución arts. 24 and 31, Decretos 57-2008, 47-2008 and 006-2003) with notes for visitors from the EU, UK, Brazil and California. Have a Guatemalan lawyer review them, and fill in the legal entity, before relying on them.

Where things live​

PieceLocation
Document text (es + en)apps/landing-page/src/content/{es,en}/{terms,privacy,cookies,refunds}.ts
Renderer (one for all four)apps/landing-page/src/components/legal/LegalDocument.tsx, routed by components/templates/LegalPageTemplate.tsx
Legal entity, contact, versionapps/landing-page/src/content/legal-identity.ts
Shared version and slugspackages/global/constants/legal.ts (PWA + backend)
Cookie banner and consent storeapps/landing-page/src/components/consent/, src/lib/consent.ts
Lead form consent fieldsapps/landing-page/src/components/forms/ConsentFields.tsx
PWA links and acceptanceapps/frontend-pwa/src/components/legal/ (LegalLinks, AcceptTermsGate)

The PWA does not keep its own copy of any document. It links to the landing page (getLegalDocumentUrl in src/lib/landing-site.ts), so each policy has exactly one published text.

One version for the set​

LEGAL_DOCUMENT_VERSION (an ISO date) versions the four documents together — a merchant accepts the set.

  • The backend accepts only the current value (@IsIn([LEGAL_DOCUMENT_VERSION])) on leads and on user acceptance, so a stale bundle fails loudly instead of recording consent to a document that is no longer published.
  • The landing page is built without packages/global and keeps a copy in legal-identity.ts. legal-pages.test.ts reads the global file and fails if the two differ.

Changing a document​

  1. Edit the text in both locales and update lastUpdatedDate.
  2. If the change is material, bump LEGAL_DOCUMENT_VERSION in both packages/global/constants/legal.ts and apps/landing-page/src/content/legal-identity.ts, in the same commit.
  3. Deploy the landing page first, then the backend and the PWA (see Deploy order).

A bump re-prompts every signed-in user through AcceptTermsGate, and lead forms start sending the new version.

What is recorded​

WhereColumnsSet by
leadprivacy_policy_version, privacy_accepted_at, marketing_opt_inPOST /leads; the server stamps the time
userterms_version, terms_accepted_atPOST /auth/firebase with acceptedTermsVersion (sign-up checkbox), or POST /auth/accept-terms (the gate)

Rows captured before these columns existed stay null: backfilling a timestamp would claim a consent nobody gave. marketing_opt_in defaults to false, and only leads with it set may receive marketing — the platform leads screen exposes it.

POST /auth/accept-terms resolves the user from the verified token, never from the body, and is idempotent: accepting the version already on record keeps the original timestamp.

Who is asked, and how​

  • Lead forms (demo, contact, restaurants inline): a required, unticked privacy checkbox and a separate, unticked marketing checkbox that names email and WhatsApp.
  • PWA sign-up: a required checkbox. Google sign-up is blocked until it is ticked; the version survives a Google redirect in sessionStorage.
  • PWA sign-in: a notice only, nothing recorded. An account created from the sign-in tab (a first Google sign-in) is asked by AcceptTermsGate instead, so every stored acceptance comes from a deliberate act.
  • AcceptTermsGate, mounted in PrivateRoute: blocks onboarding and the app until the stored version matches. It cannot be dismissed — only accepted or signed out of.

GA4 and the Meta Pixel load only after the visitor opts in, for every visitor. The earlier EU-only geo-block missed the UK, the rest of the EEA, Switzerland, Brazil and California.

  • The choice lives in the first-party cookie flowpos_consent (180 days, no personal data). Bump CONSENT_VERSION in lib/consent.ts when a category gains a new vendor; that re-asks everyone.
  • Accept and Reject share one style on purpose — unequal buttons invalidate consent.
  • "Cookie settings" in the footer reopens the banner. Withdrawing deletes the _ga* / _fbp cookies and switches the loaded tools off.
  • trackEvent also checks consent, so a tracker left on the page after a withdrawal receives nothing more.
  • Adding a tracker: gate it on a category in AnalyticsProvider, add its cookies to the Cookie Policy table, and bump CONSENT_VERSION.

The PWA shows no banner: everything it stores is needed to provide the service, and it loads no analytics or advertising tools. Its Inter font is self-hosted. Sign-in backgrounds are mostly images.unsplash.com, which the browser loads directly, so Unsplash receives the visitor's IP address before sign-in. Four local photos from the landing page are in the same rotation.

Deploy order​

Landing page → backend → PWA. The backend's GlobalValidationPipe strips unknown fields, so an old backend accepts leads from the new landing page (without storing consent). The new backend requires the consent fields, so deploying it first would reject every lead from the old landing page.

Tests that guard this​

TestGuards
landing-page/.../legal-pages.test.tsEvery document loads in both languages; anchors unique; one contact address; version and slugs match packages/global
landing-page/.../AnalyticsProvider.test.tsxNothing loads before a decision or after Reject; each tool gated on its own category
landing-page/.../consent-fields.test.tsxNo lead is sent without the privacy checkbox; marketing defaults off
landing-page/.../text-contrast.test.ts, frontend-pwa/.../theme-contrast.test.tsText tokens clear 4.5:1 in both themes
backend/.../create-lead.dto.spec.ts, terms-acceptance.dto.spec.tsMissing, false or stale consent is rejected
frontend-pwa/.../SignInPage.test.tsx, AcceptTermsGate.test.tsxSign-up and Google sign-up blocked without the checkbox; the gate cannot be dismissed; the sign-in background is a local /images/sign-in/ photo

Known gaps​

  • The legal entity (razón social, NIT, address) is a [SLOT] in legal-identity.ts.
  • There is no data processing agreement for merchants, whose customers' and employees' data (payroll, national IDs) FlowPOS processes.
  • There is no retention job for leads; the Privacy Policy describes retention as practised, not automated.