# Business rules — VivaInnovations (Vivlia)

Evidence date: 2026-09-29. Rules are from branch `dev` at `71b4967`.

Labels: **Documented**, **Implemented**, **Inferred**. Rules that the code suggests but that no document states are **Inferred**. Conflicts are marked **Requires Confirmation**.

## Identity and access

### BR-001 — Email is the username

**Implemented.** Cognito `usernameAttributes` is email. Required attribute is email. Email is auto-verified. Evidence: `amplify/backend/auth/vivliaa1cb2768/parameters.json`.

### BR-002 — Password length

**Implemented.** Minimum length 8. `passwordPolicyCharacters` is empty, so no required uppercase, number, or symbol is configured in Amplify. Evidence: same parameters file.

### BR-003 — MFA is off

**Implemented.** `mfaConfiguration` is `OFF`. SMS MFA is listed as a type but not enabled.

### BR-004 — Self-registration creates a consumer

**Implemented.** After confirmation, the app inserts `User.role = CONSUMER`. The post-confirmation trigger adds Cognito group `CONSUMER`. Evidence: `authHelpers.ts`, `add-to-group.js`.

**Documented** rule that end users are role USER and register on a mobile app: not implemented in this repo. **Requires Confirmation.**

### BR-005 — Portal staff are invited

**Documented.** Super Admin invites Super Admin. Super Admin or Admin invites Admin.

**Implemented.** The new-user form can create Admin, Agent, or Super Admin. The lambda `createUser` calls `adminCreateUser` and `adminAddUserToGroup` using the group name in the body. The lambda does not check that the caller is a Super Admin or Admin. **Inferred:** any caller who can invoke the API Gateway method can create a portal user in any group name they send.

### BR-006 — Who sees the admin portal

**Implemented.** The first Cognito group on the access token is the role. `ADMIN`, `SUPER_ADMIN`, and `AGENT` receive `ADMIN_ROUTES`. Every other value receives `CONSUMER_ROUTES`. Evidence: `jwtInfo.ts`.

**Inferred.** Agent is an admin for navigation. The README does not say this.

**Inferred.** Super Admin and Admin share one menu. The documented "limited" admin portal has no second route table.

### BR-007 — Private pages require a local token

**Implemented.** `AuthRoute` sends the browser to `/sign-in` when `isLoggedIn()` is false. That function reads `localStorage`. It does not, by itself, prove the Cognito session is still valid.

### BR-008 — Unauthenticated AWS identities are allowed

**Implemented.** `allowUnauthenticatedIdentities` is true. AppSync default authentication is `AWS_IAM`. GraphQL types have no `@auth` directive.

**Inferred.** Row-level "users only see their own cart" is not expressed in the schema. Whether the unauthenticated IAM role can query or mutate DynamoDB is **Unknown / Requires Confirmation**. The auth CloudFormation template does not mention AppSync.

### BR-009 — Disable versus archive

**Implemented.** Edit user can set DynamoDB status to `ARCHIVED`. The disable lambda calls Cognito `adminDisableUser` and does not update DynamoDB. These are separate mechanisms. Whether the archive button calls the lambda is **Unknown / Requires Confirmation**.

## Catalogue

### BR-010 — Resource kinds

**Implemented.** `ResourceType`: `TEXTBOOK`, `LITERATURE`, `LEGAL_DOCUMENT`, `LEARNING_MATERIAL`.

Literature categories: `CHILDREN`, `YOUNG_ADULT`, `ADULT`. Learner literature submenu links to `/learner/literature/children`, `young-adult`, and `adult`.

### BR-011 — Published means Active

**Implemented.** Admin filters label `Status.Active` as "Show Published". Public lists query `status: ACTIVE`. Draft and Archived are admin filters. `SUSPENDED` exists on the enum. A dedicated suspended workflow was not found. **Inferred:** `SUSPENDED` is unused by the screens inspected.

### BR-012 — Legal documents stay out of public search

**Implemented.** Public search filter uses `type: { ne: LEGAL_DOCUMENT }` and `status: ACTIVE`. Evidence: `Landing/Search.tsx`.

### BR-013 — Prices are strings

**Implemented.** `price`, `salePrice`, `voucherPrice`, and transaction `cost` are GraphQL `String`. The cart adds them with unary plus. **Inferred:** there is no currency minor-unit type and no server-side price check before Peach is called. The amount sent to Peach is `totalCost` from the decrypted client payload.

### BR-014 — Languages and subjects are closed lists

**Implemented.** `Language` lists official South African languages (Sepedi, Sesotho, Setswana, Siswati, Tshivenda, Xitsonga, Afrikaans, English, isiNdebele, isiXhosa, isiZulu). `Subject` lists school subjects, including Afrikaans duplicates such as `GESKIEDENIS` and `REKENINGKUNDE`. `NULL` is a subject and a genre value.

### BR-015 — Grades

**Implemented.** `GRADE_R` through `GRADE_12`, plus `GENERAL`, `RESOURCE`, and `CATCH_UP`.

## Commerce

### BR-016 — Currency and brands

**Implemented.** Peach payload `currency` is `ZAR`, `paymentType` is `DB` (debit). Brands offered in the UI are Mastercard (`MASTER`) and Visa (`VISA`). Evidence: `PAYMENT_TYPES`, `makePayment/src/index.js`.

### BR-017 — Checkout re-authenticates

**Implemented.** Cart checkout calls `Auth.signIn` with the signed-in email and a password the learner types again, before payment. Evidence: `Cart/index.tsx`.

### BR-018 — Transaction states

**Implemented.** `PENDING`, `COMPLETE`, `FAILED`. Sales admin can filter each. Profile purchase history shows `COMPLETE` only.

### BR-019 — Voucher use limit

**Implemented.** Redeem is refused when `maxUses <= transactions.items.length`, with the message "This voucher has already been used". The comparison uses the length of the transactions connection returned on the voucher, not a count of completed redemptions only. **Inferred:** pending or failed transactions count toward the limit if they are connected.

### BR-020 — Voucher must be active to be looked up for redeem

**Implemented.** The redeem query filters `status: ACTIVE` and exact `code`. A missing id surfaces "Invalid Voucher" / "Your voucher is invalid".

### BR-021 — Expiry is stored and not applied on redeem

**Inferred.** `Voucher.expiryDate` is a `String` on the model and is not read in `checkVoucherValidity`. A voucher past that date can still be redeemed if it is active and under `maxUses`.

### BR-022 — Subject voucher discount

**Implemented.** If a cart connection's voucher has `subjectVoucher` and `voucherPrice`, that price string is stored as the discount. Resource lists show "Subject Price" from `voucherPrice` when `subjectVoucher` is true and the price string is not `'0'`.

### BR-023 — What the learner pays is decided in the browser

**Inferred.** The lambda charges `totalCost` from the client payload. It does not reload the cart from DynamoDB. A mismatch between catalogue price and charged amount would not be rejected by this function.

## Communications

### BR-024 — Email is a generic SES send

**Implemented.** Body fields `to`, `subject`, and `message` (HTML). From address is environment variable `ADMIN_EMAIL`. The committed API definition marks `/send-email` as open (`sendEmailApi/api-params.json`, `open: true`). **Inferred:** the lambda does not check a caller identity before sending.

**Documented** and not found as fixed templates: a payment email and a user-verified email. **Requires Confirmation** whether another service still sends those templates.

### BR-025 — Push notifications

**Documented** in the README. **Not implemented** in the `dev` tree. Do not treat push as a current rule.

## Content operations

### BR-026 — New catalogue items can be saved as draft or active

**Implemented.** User, unit, and topic forms set `DRAFT` or `ACTIVE` from the save action. Archive sets `ARCHIVED`.

### BR-027 — Contact page does not define a ticket workflow

**Implemented.** `/contact-us` renders contact and about sections. No case-management model exists in the schema. What happens to a submitted message is **Unknown / Requires Confirmation** beyond the React section components.

## Process rules (SovTech, documented)

`CONTRIBUTING.md` and `RDSA.md` state Gitflow, Angular-style commit messages, and pull-request review. **Documented.** History through 2021 shows Bitbucket pull requests into the application branches. Those documents also say the default branch is `develop` and that direct pushes to `master` are forbidden. Current HEAD is an orphan `master` commit. **Requires Confirmation** whether those repository settings still exist.

## Explicit non-rules

No business rule was found for tax, refunds, partial capture, subscription billing, or school-roster licensing. Do not invent them.
