# PRD — VivaInnovations (Vivlia)

Evidence date: 2026-09-29. Product behaviour below is from branch `dev` at `71b4967` unless a section says otherwise.

Provenance labels: **Documented** (README or other in-repo doc), **Implemented** (code on `dev`), **Inferred**, **Planned**, **Unknown / Requires Confirmation**.

`master` (`37b1a24`) does not contain this product. See `project-plan.md`.

## Product purpose

Vivlia is a web catalogue and reader for digital school textbooks, literature, and learning material, with an admin portal for catalogue, users, vouchers, and sales.

**Documented** in `README.md`. **Implemented** in `src/navigation/routes.ts` and `amplify/backend/api/vivlia/schema.graphql`.

The repository is named VivaInnovations. The in-app title and README name are Vivlia. The production hostname in the README is `www.vivinnovation.co.za`.

## Users and roles

### Documented (`README.md`)

| Role | Documented access |
|---|---|
| SUPER_ADMIN | Invited by another Super Admin. Full admin portal. |
| ADMIN | Invited by Super Admin or Admin. Limited admin portal. |
| USER | End user. Registers with the mobile app. |

### Implemented

GraphQL enum `UserRole`: `SUPER_ADMIN`, `ADMIN`, `AGENT`, `CONSUMER`, `VISITOR`.

`src/utils/jwtInfo.ts` reads the first `cognito:groups` value on the access token. `isAdmin` is true for `ADMIN`, `SUPER_ADMIN`, and `AGENT`. Anyone else, including `CONSUMER` and a missing group, gets the learner navigation.

Cognito post-confirmation (`add-to-group.js`) adds every confirmed sign-up to the group `CONSUMER`, creating that group if it is missing.

Web registration (`src/utils/authHelpers.ts`) writes a DynamoDB `User` with `role: CONSUMER` and the Cognito `sub` as `id`.

Admin user creation (`src/containers/Users/newUser.tsx`) offers roles Admin, Agent, and Super Admin, default Admin, then calls the `createUser` lambda with the user-pool id from `aws-exports`.

**Discrepancy:** README role `USER` is not in the enum. Implemented learner role is `CONSUMER`. README does not mention `AGENT` or `VISITOR`. `AGENT` is treated as an admin in the UI. No code path was found that grants Super Admin a larger admin menu than Admin or Agent. "Limited admin portal" is **Documented** and was not found as a separate route set. **Requires Confirmation.**

**Discrepancy:** README says end users register on the mobile app. **Implemented** registration is the web route `/register`. No mobile client is in this repository.

## Major workflows

### Visitor browses the public site

Public routes in `src/navigation/routes.ts`: `/` (landing, title Vivlia), `/textbook`, `/literature`, `/learning-material`, `/explore/:resourcetype/:grade/:subject/:resource`, `/search`, `/faqs`, `/contact-us`, `/sign-in`, `/forgot-password`, `/register`.

Public resource queries filter `status: ACTIVE`. Public search also excludes `ResourceType.LEGAL_DOCUMENT` (`src/containers/Landing/Search.tsx`).

### Learner registers and signs in

1. `Auth.signUp` with email as username. Cognito required attribute is email. Auto-verified attribute is email. MFA is `OFF`. Password minimum length is 8. No character-class rules (`amplify/backend/auth/vivliaa1cb2768/parameters.json`).
2. User enters the confirmation code. `confirmSignUp` then signs in and creates the DynamoDB user as `CONSUMER`.
3. Post-confirmation trigger adds the Cognito user to group `CONSUMER`.
4. Access token is stored in `localStorage` under the auth token key. Sign-in clears `localStorage` first.
5. Unconfirmed sign-in (`UserNotConfirmedException`) tells the user to use the login screen. Duplicate username offers resend of the code.
6. Forgot-password uses `Auth.forgotPassword` and `Auth.forgotPasswordSubmit`.
7. After login, `/limbo` sends admins to `/admin/dashboard` and everyone else to `/learner/your-library`.

Private routes redirect to `/sign-in` when `isLoggedIn()` is false (`src/navigation/index.tsx`). That check is a token in `localStorage`. It is not a server session check.

### Learner buys a resource

**Implemented** in `src/containers/Cart/index.tsx` and `amplify/backend/function/makePayment/src/index.js`.

1. Cart rows are `UserCartConn` between `User` and `Resource`.
2. Cart total is the sum of resource `price` strings coerced to numbers. A subject voucher on a cart row sets a discount from `voucherPrice`.
3. Checkout asks for the learner password and calls `Auth.signIn` again.
4. The learner enters card holder, number, expiry month, expiry year, CVV, and brand (`MASTER` or `VISA`).
5. The browser encrypts a payload and `POST`s it to API Gateway `/make-payment`. The lambda decrypts it. Activity `makePayment` sends a Peach debit (`paymentType: DB`, `currency: ZAR`) and returns a redirect URL and Peach transaction id. Activity `checkPayStatus` GETs a status URL and returns Peach `result`.
6. Route `/learner/cart/transaction/payment/status/:transid` shows the result.
7. A `Transaction` is created with `peachPaymentReference`, `status` (`PENDING`, `COMPLETE`, `FAILED`), `cost`, user, optional voucher, and resource connections.

The Express file `makePayment/src/app.js` is the Amplify sample and does not perform a payment. The handler is `index.js`.

### Learner redeems a voucher

**Implemented** in `src/containers/MyLibrary/index.tsx`.

The learner enters a code. The app loads vouchers with `status: ACTIVE` and matching `code`. If `maxUses` is less than or equal to the number of related transactions, the UI says the voucher has already been used. Otherwise it lists the voucher's resources.

`expiryDate` is on the model. This redeem path does not compare it to the current date.

### Learner reads the library

`/learner/your-library` lists purchased resources. PDF files are opened from a signed S3 URL. EPUB files are read from local storage (`localForage`) after download. The reader can highlight and underline (`Annotation` model and epub annotations). Purchase history on the profile lists transactions whose status is `COMPLETE`.

### Admin manages the portal

Sidebar when `isAdmin` is true:

| Path | Screen |
|---|---|
| `/admin/dashboard` | Counts and a sales chart |
| `/admin/users` | Users, filter by role |
| `/admin/textbook-management` | Textbooks, including `/new` and `/:id` |
| `/admin/literature-management` | Literature, including `/new` and `/:id` |
| `/admin/learning-material` | Topics and units |
| `/admin/sales` | Transactions filtered by status |
| `/admin/voucher-management` | Vouchers |
| `/admin/content-management` | Legal documents (`ResourceType.LEGAL_DOCUMENT`) |
| `/admin/faq-management` | FAQs |
| `/admin/user/:id` | Edit user |
| `/:platform/profile` | Profile |

Catalogue screens default the status filter to `ACTIVE` and label it "Show Published". Other filters: Draft, Archived. User status can be set to Draft, Active, or Archived. Archived users cannot be archived again from the edit screen (the control is disabled).

The dashboard copies `DUMMYSALESDATA` (twelve months, sales starting at 0) and increments the month bucket from `listTransactions`. The chart is **Implemented** as live counts on a fixed month template, not as a stored dummy dataset left unchanged.

## Functional requirements

### FR-001 — Public catalogue

The system shows landing, textbook, literature, and learning-material catalogues, subject and grade drill-down, resource detail, public search, FAQs, and a contact page.

- Actor: visitor.
- Trigger: open a public route.
- Behaviour: list or show resources with `status` equal to `ACTIVE`. Public search omits legal documents.
- Output: HTML screens.
- Evidence: `src/navigation/routes.ts`, `Landing/Search.tsx`, `ResourceExploration/`.
- Implementation: **Implemented** on `dev`.
- Confidence: High.

### FR-002 — Learner account

A person can register with email and password, confirm the email code, sign in, reset a password, and change a password while signed in.

- Actor: visitor, then learner.
- Inputs: email, password, confirmation code.
- Validation: Cognito password length at least 8. Email is the username and is auto-verified.
- Errors: Cognito error messages are shown. Unconfirmed users are told to use the login screen. A bad confirmation code offers resend.
- Side effect: DynamoDB `User` with role `CONSUMER`; Cognito group `CONSUMER`.
- Evidence: `authHelpers.ts`, `parameters.json`, `add-to-group.js`.
- Implementation: **Implemented**.
- Confidence: High.

### FR-003 — Admin and agent accounts

An admin can create another portal user in Cognito and add them to a group named by the form (Admin, Agent, or Super Admin).

- Actor: a signed-in user who passes `isAdmin` (includes Agent).
- Trigger: `/admin/users` new-user form.
- Inputs: user pool id, username, group name, sent as JSON to `createUser`.
- Expected behaviour: `adminCreateUser`, then `adminAddUserToGroup`.
- Evidence: `newUser.tsx`, `createUser/src/index.js`.
- Implementation: **Implemented**. The lambda does not check the caller's role. API Gateway marks `/create-user` private to the auth role (`createAdminUser/api-params.json`). Whether the unauthenticated identity can call it is **Unknown**.
- Confidence: Medium. Role restriction is not in the lambda.

### FR-004 — Disable a user in Cognito

The disable-user lambda calls `adminDisableUser` with `userPoolId` and `userName` from the body.

- Evidence: `disableUserAccount/src/index.js`. The edit-user screen sets DynamoDB status to Archived. Whether that screen also calls this lambda was not fully traced. **Requires Confirmation** for the UI trigger.
- Implementation: lambda **Implemented**. UI trigger **Unknown**.

### FR-005 — Catalogue administration

An admin can create and edit textbooks, literature, learning-material topics and units, and legal documents, and can set status Active, Draft, or Archived.

- Resource fields include title, description, author, publisher, ISBN, grade, price, sale price, document, cover, subject, genre, layout (`REFLOWABLE`, `PDF`, `EPUB`), language, type, and literature category (`CHILDREN`, `YOUNG_ADULT`, `ADULT`).
- Grades include Grade R through Grade 12, plus `GENERAL`, `RESOURCE`, and `CATCH_UP`.
- Evidence: `schema.graphql`, `ResourceManagement/`, `LearningMaterial/`, `ContentManagement/`, `contentFields.tsx` (`type: LEGAL_DOCUMENT`).
- Implementation: **Implemented**.
- Confidence: High.

### FR-006 — Purchase

A signed-in learner can check out a cart by card (Mastercard or Visa) in ZAR through Peach Payments and land on a transaction-status route.

- Evidence: `Cart/index.tsx`, `makePayment/src/index.js`, `Transaction` type.
- Implementation: **Implemented** on `dev`.
- Constraint: card data is collected in the browser and decrypted inside the lambda. See technical documentation. This requirement records the behaviour. It does not approve it.
- Confidence: High for the flow. Whether Peach is still the live gateway is **Unknown**.

### FR-007 — Vouchers

An admin can create vouchers with code, description, status, usage, price, subject-voucher flag, max uses, expiry, and linked resources.

A learner can redeem an active voucher by code when related transactions are fewer than `maxUses`.

- Evidence: `Voucher` type, `VoucherManagement/`, `MyLibrary/index.tsx`.
- Expiry is stored and not enforced on the redeem path inspected. Provenance: **Inferred** gap.
- Implementation: **Implemented** for create and redeem. Expiry enforcement: not found.
- Confidence: Medium.

### FR-008 — Library and reader

A learner can open purchased PDF and EPUB resources, download for offline use, and add highlight or underline annotations.

- Evidence: `MyLibrary/`, `Annotation` type, `filesystem.ts`, `convertEpub.js`.
- Implementation: **Implemented**.
- Confidence: High.

### FR-009 — Sales and profile

An admin can filter transactions by Complete, Pending, and Failed. A learner can see completed purchases on the profile and update profile fields and password.

- Evidence: `Sales/index.tsx`, `Profile/`.
- Implementation: **Implemented**.
- Confidence: High.

### FR-010 — Email

The system can send an HTML email through Amazon SES. The lambda reads `message`, `subject`, and `to` from the request body. Source address is `ADMIN_EMAIL`.

- **Documented** events: payment notification, and user-verified notification, via a notifier and HTML templates.
- **Implemented:** generic SES send. The API path `/send-email` is marked open in `sendEmailApi/api-params.json`. Template files are not in `sendEmail/src` (a `baseTemplate` import is commented out).
- **Discrepancy:** README services table names Mailgun for marketing email. No Mailgun client was found.
- Implementation: SES send **Implemented**. Named templates and Mailgun **not found**.
- Confidence: High for the discrepancy.

### FR-011 — Environments

**Documented:** dev (`https://dev.vivlia.sovtech.org/`) on every merge to `develop`; UAT (`https://uat.vivlia.sovtech.org/`) on every merge to `master`; production (`https://prod.vivlia.sovtech.org/` or `https://www.vivinnovation.co.za/`) promoted manually from UAT. Production is described as live data, payments, and communications.

**Implemented** in `bitbucket-pipelines.yml` on `dev`:

- Custom pipelines: `deploy-to-dev` (deployment `test`), `deploy-to-uat` (`staging`), `deploy-to-live` (`production`).
- Branch `dev`: automatic deploy to DEV (`test`), then SonarQube, then manual UAT (`staging`), then manual LIVE (`production`).
- Branch `master`: automatic deploy to UAT, then manual LIVE.

`deploy.sh` runs `amplify init`, `env pull`, `push`, `codegen`, and `publish`. Amplify env name is `${STAGE}`.

**Discrepancy:** README branch mapping does not match the pipeline file. `master` has no application to deploy. Amplify env names in git are `dev`, `uat`, and `staging`, not `prod`.

- Implementation: pipeline **Implemented** as YAML. Whether Bitbucket still runs it is **Unknown**.
- Confidence: High for the text conflict.

## Integrations

| System | Purpose | Direction | Auth as found | Failure behaviour |
|---|---|---|---|---|
| Amazon Cognito | Sign-up, sign-in, groups | App and lambdas to Cognito | Amplify; lambdas use the execution role | Cognito errors returned to the UI or lambda callback |
| AWS AppSync | GraphQL over DynamoDB | Browser to AppSync | `AWS_IAM` via `Auth.currentCredentials()` | Apollo `errorPolicy: 'all'` |
| Amazon S3 | Documents, covers, web hosting | Browser `Storage` and signed URLs | Amplify storage; `signedUrls.js` also builds an S3 client from `REACT_APP_AWS_*` env vars | Errors returned to the caller |
| Amazon CloudFront | Distribute the web app and media | Documented in README | ACM certificates, documented | Unknown at runtime |
| Peach Payments | Card debit and status | Lambda to `peachUrl` from the decrypted body | `Authorization` header from the decrypted body | No local fallback. A missing `redirect` throws inside the lambda. |
| Amazon SES | Transactional HTML mail | Lambda | IAM, region from `process.env.REGION` | `callback(err, event)` |
| Bugsnag | Front-end error reporting | Browser | API key env var. `constants/index.ts` assigns the string `'process.env.REACT_APP_BUGSNAG_API_KEY'` rather than reading the env var. **Inferred:** Bugsnag is misconfigured in that constant. | Unknown |
| Google Analytics | Page tracking | Browser | `REACT_APP_GOOGLE_ANALYTICS_TRACKING_ID` | `GA.init()` gates the tracker |
| SonarQube | Static analysis | Pipeline `sonar-scanner` | `SONARQUBE_URL`, `SONARQUBE_TOKEN` | Pipeline step failure |

README also names Route 53 for `vivinnovation.co.za`, CloudWatch for lambda logs, and Mailgun. Route 53 and CloudWatch are **Documented**. Mailgun was not found in code.

## Constraints

- GraphQL models have no `@auth` rules. Access depends on IAM roles for the identity pool. The identity pool allows unauthenticated identities. Fine-grained per-user data rules are not in the schema.
- Prices, costs, and voucher prices are GraphQL `String`, not a money type.
- `Resource` is `@searchable`. Other models are not.
- Pipeline unit tests are commented out (`# yarn test:ci --coverage`).
- Husky on `dev` runs `tsc --noEmit` and lint-staged on pre-commit, and `yarn test:ci` on pre-push.
- README embeds values that look like live credentials (AWS access key id, Bugsnag key, analytics id, admin mailbox). Those values are omitted here.

## Non-goals of this document

This PRD does not choose a future branch strategy, a production environment, or a replacement for the payment design. Those are confirmation items in `project-plan.md`.
