# Business rules — click-learner-portal-platform

Evidence date: 2026-10-18.

Source: `master` at `99bde51`, with **Production branch** called out where `origin/Production` (`131cc59`) differs.

There is no business-rules document in the source repository. Every rule below is **Inferred** from implementation unless the provenance column says otherwise. A comment in code is not an approved product rule.

Rules that could not be established are listed at the end rather than filled in.

## Launch eligibility

| ID | Rule | Provenance |
|---|---|---|
| BR-001 | Any caller who can request `GET /default/{learnerId}/{quizId}/{quizEndDate}` receives a launch. No session, password, API key, or role is checked. | **Inferred** from `security.yaml` (no access control) and `DefaultController` (no check). |
| BR-002 | The three path segments are opaque strings. Empty segments cannot match the route. Format, length, and character set are not validated. | **Inferred** from the route pattern and the absence of validator constraints. |
| BR-003 | The app does not check that `{learnerId}` exists, that `{quizId}` is a known quiz, or that `{quizEndDate}` is in the future. | **Inferred** from the controller body. |

## Who the launch says the user is

| ID | Rule | Provenance |
|---|---|---|
| BR-004 | On the TAO route, the LTI login hint is the `{learnerId}` path segment. | **Inferred** from the third argument to `buildLtiResourceLinkLaunchRequest`. |
| BR-005 | On the TAO route, the LTI role is `http://purl.imsglobal.org/vocab/lis/v2/membership#Learner`. No other role is sent. | **Inferred** from the roles array. |
| BR-006 | On the TAO route, deployment id is the string `1`, which is also the only deployment id on registration `tao`. | **Inferred** from the controller argument and `lti1p3.yaml`. |
| BR-007 | The resource-link identifier is the literal `resourceLinkIdentifier` for every learner and every quiz. A comment says it needs to be unique. The code does not vary it. | **Inferred** from the constructor argument. Whether uniqueness is required by TAO is **Unknown**. |
| BR-008 | `UserAuthenticator` does not load a person. On success it sets the user id to the login hint and the display name to `Maanie`. | **Inferred** from `UserIdentity($loginHint, 'Maanie')`. The `//TODO:` comment shows the author did not treat the lookup as done. |
| BR-009 | The TAO route does not call `UserAuthenticator`. BR-008 applies to the bundle authenticator service and not to `DefaultController`'s launch. | **Inferred** from the two call paths. |
| BR-010 | The API Platform route forces an anonymous identity (`UserAuthenticator(true, true)`). Its login hint is the literal `loginHint`, not `{learnerid}`. | **Inferred** from `LtiRequestController`. A comment says the anonymous flag is testing and "needs to be false". |

## Quiz target and return

| ID | Rule | Provenance |
|---|---|---|
| BR-011 | The tool URL for the TAO route is `https://testrunner-ignite.prod.gcp-eu.taocloud.org/api/v1/auth/launch-lti-1p3/` plus `{quizId}`. | **Inferred** from the resource-link URL. |
| BR-012 | The `launch_url` on tool `taoTool` in yaml, which ends in quiz id `04fc0ce15e09`, is not the URL `DefaultController` launches. | **Inferred** from comparing the yaml value with the controller. |
| BR-013 | On `master`, the tool is told to return the browser to `https://portaltest.clicklearning.org/click-home-learning` with query `learner_id` and `quiz_id` equal to the path segments. | **Inferred** from the `return_url` claim. |
| BR-014 | On **Production branch**, BR-013 uses host `learn.clicklearning.org` and the same path and query. | **Inferred** from `DefaultController.php` at `131cc59`. |
| BR-015 | The custom claim `deliverySettings.closeOn` is `{quizEndDate}` concatenated with `T00:00:00+00:00`, so the clock time is always midnight UTC. | **Inferred** from the concatenation. That midnight UTC is the intended close instant **Requires Confirmation**. |
| BR-016 | Client id presented for the TAO registration is `click-learning`. Platform audience is `https://clms.clicklearning.org/platform`. | **Inferred** from `lti1p3.yaml`. |

## Local and API Platform path

| ID | Rule | Provenance |
|---|---|---|
| BR-017 | `GET /api/ltiresourcelink/{learnerid}` does not read an entity (`read: false`). `ltirequest` is not mapped as a Doctrine entity, so there is no row to load. | **Inferred** from the API Platform operation and the missing `@ORM\Entity`. |
| BR-018 | That route targets `http://127.0.0.1:8001/tool/launch` and registration `local`, whose client id is the literal `common_client_id_value`. | **Inferred** from `LtiRequestController` and `lti1p3.yaml`. |
| BR-019 | The response body is a JSON encoding of a URL. The HTML form return is commented out. Content type is not set to JSON. | **Inferred** from the active `return` and the commented one. |
| BR-020 | `quiz_id` on `ltirequest` is not an input to this operation. The path has only `{learnerid}`. | **Inferred** from the operation path and the unused property. |

## Access, CORS, and environment

| ID | Rule | Provenance |
|---|---|---|
| BR-021 | Symfony security does not assign roles and does not protect any path. | **Inferred** from `security.yaml`. |
| BR-022 | Cross-origin browser calls are allowed only from `http` or `https` localhost or `127.0.0.1`, any port. Methods include GET, OPTIONS, POST, PUT, PATCH, and DELETE. | **Inferred** from `CORS_ALLOW_ORIGIN` and `nelmio_cors.yaml`. |
| BR-023 | Committed `.env` sets `APP_ENV=dev` on both branch tips. | **Implemented** file content. |
| BR-024 | **Production branch** commits `.env.local.php` with `APP_ENV` `prod`. Symfony's comment in `.env` says the compiled file is the production override. `.env` on that branch is still `dev`. | **Inferred** that a deploy of `Production` which keeps `.env.local.php` boots as prod. **Requires Confirmation** against the host. |
| BR-025 | **Production branch** `scripts/start_server` exports `SYMFONY_ENV=prod` after starting `httpd`. The application secret and kernel read `APP_ENV` (`config/packages/framework.yaml`, `public/index.php`). The export is not written into an Apache environment file. | **Inferred** that this line does not by itself switch the PHP app to prod. |

## Deployment rules visible in the pipeline

| ID | Rule | Provenance |
|---|---|---|
| BR-026 | A push to `master` deploys using the `QA_*` CodeDeploy variables. A push to `Production` deploys using the `PROD_*` variables. No other branch has a pipeline. | **Inferred** from `bitbucket-pipelines.yml`. |
| BR-027 | Every deploy uses version label `my-app-1.0.0`. The pipeline does not put the git sha in the label. | **Inferred** from the constant. Whether CodeDeploy rejects a repeated label is **Unknown** from this repo. |
| BR-028 | Application stop failures are ignored (`IGNORE_APPLICATION_STOP_FAILURES: 'true'`). | **Inferred** from the pipeline variable. |
| BR-029 | Files are copied over the existing tree (`FILE_EXISTS_BEHAVIOR: 'OVERWRITE'`) into `/var/www/html/clms.clicklearning.org/` as user and group `apache`. | **Inferred** from the pipeline variable and `appspec.yml`. |
| BR-030 | BeforeInstall runs `scripts/install_dependencies`, whose only command is a commented `yum install httpd`. The hook does not install PHP extensions or run Composer. | **Inferred** from the script. `vendor/` is tracked, so a zip of the repo already contains dependencies. |
| BR-031 | ApplicationStart runs `service httpd start`. It does not reload a specific vhost and does not run migrations. | **Inferred** from `scripts/start_server` and the absence of a migrate hook. |
| BR-032 | `scripts/remove_archives.sh` deletes CodeDeploy deployment-root directories other than the last successful install. | **Inferred** from the script. |

## Data the application does not keep

| ID | Rule | Provenance |
|---|---|---|
| BR-033 | The launch is not stored. There is no table, log write, or audit record in application code. | **Inferred** from the controllers. PHP error logging is enabled (`framework.yaml` `php_errors.log: true`) but no launch log was added. |
| BR-034 | `DATABASE_URL` points at PostgreSQL database `app` on `127.0.0.1:5432` with the Doctrine recipe user. No code path uses it. | **Inferred** from `.env` and the absence of queries. |

## Rules that are not established

- Who is allowed to launch a quiz, and whether the learner id must be the signed-in learner.
- Whether `portaltest.clicklearning.org` or `learn.clicklearning.org` is the approved return host.
- The required format of `{quizEndDate}`, and the time zone of the close instant.
- Whether TAO requires a unique resource-link id, and what happens when every launch reuses `resourceLinkIdentifier`.
- Whether registration `local` and `/api/ltiresourcelink/{learnerid}` are allowed on the deployed host.
- Whether the display name `Maanie` was ever replaced in an environment outside git.
- Whether showing or launching with a raw learner id in the URL is an approved privacy rule.
- Whether the committed private key is a development key that was also deployed, and whether it is still trusted by TAO.
- How this app relates to ClickPortal. Shared domain only. No call in either direction was found in the files read for this set.
