SRS: UC_4.6.2 — Settings: Profile & Preferences
UC_4.6.2: Settings — Profile & Preferences
| Field | Value |
|---|---|
| BA in Charge | Anh Hoang |
| Date Created | 2026-08-11 |
| Version | v14 |
| Document References | RFQ_ Stack Trading Prop Tech V7.pdf (§Page 36 — Get User Profile / Update User Profile) · RFQ_ Website and Dashboard Implementation V7.pdf (§Part C — Settings & Profile Module) · Zapier Integration V7.pdf (§Users table schema — profile_photo_url, phone_number, ui_preferences) |
Document References
| # | Original Document | Key Sections Used |
|---|---|---|
| 1 | RFQ_ Stack Trading Prop Tech V7.pdf | §Page 36 — Get User Profile (GET user_id → {profile_photo_url, email, phone_number, ui_preferences}, "Fetches profile and configuration data from the Users table to populate the Account Settings screen") and Update User Profile (POST user_id, profile_photo_url (Optional), email (Optional), phone_number (Optional), ui_preferences (Optional JSON) → HTTP 200 OK, "Supports image uploads via S3 signed URL execution before submission") |
| 2 | RFQ_ Website and Dashboard Implementation V7.pdf | §Part C — Settings & Profile Module ("Includes Change Password, AI Chat Toggle, Billing Portal Link, Update Email, Update Phone… Must include a 'Reduce Motion' accessibility toggle") |
| 3 | Zapier Integration V7.pdf | §Users table schema — profile_photo_url (String), phone_number (String), ui_preferences (JSON Object) |
| 3b | Sotatek BE team — API review, 2026-08-21 | Three changes to the User Profiles endpoints, superseding the verb and shape given in row 1: (a) POST → PATCH for RESTful correctness; (b) the photo upload and its submission are one uninterruptible operation, not "upload before submission"; (c) the single write endpoint is split into PATCH /profile-photo and PATCH /profile. Recorded in BR_4.6.2.15. |
| 3c | BA / client answers, 2026-08-27 (QnA Init Docs — UC_4.6.1–4.6.4: Settings, Phần II) | Closes the four items UC_4.6.2 carried into launch. [PP-EP-01] confirmed — the BE team's PATCH + endpoint-split + merged-upload proposal is adopted in full, retiring the WBS's PUT /profile and the RFQ's POST Update User Profile (BR_4.6.2.15). Phone change ships with no cool-down and no security gate — regex + uniqueness, then a direct write (BR_4.6.2.12). Dashboard and Discord avatars are fully independent — StackTrading never reads an avatar or a name from the Discord API, to avoid broken images and expired CDN links (BR_4.6.2.13). One discrepancy in the same answer — PATCH /profile writing ui_preferences — is not adopted, and is recorded in BR_4.6.2.15. |
| 3d | Client answer, 2026-09-03 ([CTRL-EP-05] — the purpose of ui_preferences) | Settles what the field is for, and therefore why this screen leaves it alone. Verbatim: "There are currently no active parameters to parse for this field today. It is intentionally included in the schema strictly as a future-proofing measure. Please maintain this column as an optional JSON object. This ensures the frontend team can easily save arbitrary cross-device layout preferences in future updates without requiring structural database schema changes later." The same answer re-confirms that the Desk Manager keeps a dedicated endpoint "because it controls core operational logic and costs" and that Reduce Motion "does not require an API and should be using local storage". → the field is a deliberately dormant column, not a gap; BR_4.6.2.15 updated, payload unchanged. |
| 4 | QnA from clients — STAGE 2: Dashboard & Evaluation (Settings) | Client-confirmed answers on Definition, Trigger, Business logic, UI/UX, Endpoints for the Profile & Preferences category (Stage 2 - Dashboard & Fomula (Settings).csv, rows 303–330; Adrian Stack, 2026-08-10) |
| 5 | BA design set + direct BA instruction, 2026-08-11 | 7 screen frames (default / saved / duplicate phone / same-as-current phone / invalid format / Crop Image modal / photo removed) — Figma node 7878-116947. Source of the avatar-removal fallback rule and of the three Settings-specific phone error strings. |
Design asset note: the 7 frames above are not yet committed to
References/Wireframe/Stage 2/Settings/; this document cites the Figma node until the BA commits them (Ref:QnA_init_docs.mdA-19).
1. Overview
| Field | Content |
|---|---|
| ID | UC_4.6.2 |
| Use Case | Settings — Profile & Preferences |
| Description | Inside the Account screen, the trader opens the Profile & Preferences tab to view and maintain their personal profile. The General section exposes exactly three items: Profile photo (upload with crop, or remove — editable), Email (read-only — it is the account's unique identity key and is bound to the trading-gateway account and the blacklist, so it cannot be changed after checkout), and Phone (editable, reusing the checkout phone validation block in full — input structure, format/length rules, the system-wide uniqueness check and the error copy; there are no Settings-specific phone strings any more, Ref: BR_4.6.2.3). The screen has two independent commit paths (Ref: BR_4.6.2.14): the photo commits immediately via PATCH /profile-photo — clicking [Done] in the Crop Image modal uploads and applies the new avatar on the spot, and the previously stored image is hard-deleted; the phone is staged and committed only on [Save Changes], via PATCH /profile. The two endpoints are separate by design (Ref: BR_4.6.2.15). A photo-only change therefore never marks the form dirty and never enables [Save Changes]. The saved photo is the single source for the trader's avatar everywhere in the Dashboard — sidebar user chip and the My POD widget on the LIVE Community page (Ref: BR_4.6.2.13). The profile is global per user — identical for SIM and LIVE accounts, no environment branching (Ref: BR_4.6.2.10). The Controls panel (AI Chat / Reduce Motion) rendered alongside every Settings tab is not owned by this UC (Ref: BR_4.6.2.11). |
| Zapier Flow | — |
| Zapier Table | — |
| 3rd Party | AWS S3 (profile photo object storage — uploaded directly by the browser via a backend-issued signed URL) |
Design: Figma — Profile & Preferences, node 7878-116947
2. Trigger
- The trader clicks the user chip in the sidebar footer (displaying their initials + level, e.g.
SD/Lvl 5) and selects Settings from the dropdown (Ref: UC_4.1.2 §2 Screen Description, row 6), then selects the Profile & Preferences tab (Ref: BR_4.6.2.1).
3. Pre-conditions
- The trader is authenticated and inside the Dashboard shell (Ref: UC_4.1.1 §1 Overview).
- A Users record exists for the trader, populated at checkout with
email, full name, andphone_number(Ref: UC_2.8.3 — Phase 2 Account Claim).
4. Post-conditions
The two commit paths reach their own post-conditions independently (Ref: BR_4.6.2.14).
After a photo commit (Crop Image [Done], or a confirmed [Remove Photo]):
- The Users record reflects the new
profile_photo_url— the new S3 object URL, ornullafter a removal. No other profile column is touched. - The previously stored S3 object is hard-deleted; there is no image history to roll back to (Ref: BR_4.6.2.6).
- Every surface that renders the trader's avatar — the sidebar user chip and the
My PODwidget on the LIVE Community page — reflects the new photo, or the trader's initials if the photo was removed (Ref: BR_4.6.2.13). - The form's dirty state is unchanged — a photo commit neither enables nor resets [Save Changes] / [Cancel] (Ref: BR_4.6.2.8).
- The trader has been told the write succeeded: the success toast Ref: TS-DASH-01 — "Changes saved successfully" — is shown once the overlay closes (Ref: BR_4.6.2.16).
After a phone commit ([Save Changes] returns HTTP 200):
- The Users record reflects the submitted
phone_number; every other profile column is unchanged. - The form returns to a clean (non-dirty) state — [Cancel] is hidden and [Save Changes] is Disabled (Ref: BR_4.6.2.8).
5. Basic Flow
- The trader lands on the
Accountscreen and selects the Profile & Preferences tab. - Frontend calls
GET /profileand receives{profile_photo_url, email, phone_number, ui_preferences}. - The
Generalsection renders: the avatar (the stored photo, or the trader's initials whenprofile_photo_url IS NULL— Ref: BR_4.6.2.7), the read-only Email, and the Phone field split into dial code + number.ui_preferencesfrom the response is not consumed by this screen (Ref: BR_4.6.2.11). - The form starts clean: [Cancel] is hidden and [Save Changes] is Disabled (Ref: BR_4.6.2.8).
- The trader edits the phone number. The form becomes dirty → [Cancel] appears and [Save Changes] becomes Enabled.
Changing or removing the photo is a separate, self-committing path that runs outside this flow — Ref: §6 Alternative Flow A / Alternative Flow B and BR_4.6.2.14. A photo-only change leaves the form clean and [Save Changes] Disabled.
- On the phone field's out-click, the frontend runs, in order: format/length check (Ref: BR_4.6.2.3) → same-as-current check (Ref: BR_4.6.2.5) → uniqueness check (Ref: BR_4.6.2.4). The first failing check renders its inline error and re-Disables [Save Changes].
- The trader clicks [Save Changes].
- Frontend calls
PATCH /profilewith{user_id, phone_number}.profile_photo_urlis never part of this payload — the photo has its own endpoint (Ref: BR_4.6.2.15, §6 Alternative Flow A step 4). - On HTTP 200: success toast Ref: TS-DASH-01 is displayed and the form resets to clean per §4 Post-conditions.
6. Alternative Flows
Alternative Flow A — Change Photo
- The trader clicks [Change Photo] → the browser's native file picker opens.
- The frontend validates the selected file against BR_4.6.2.6. On failure, the matching error from Ref: CR-07 §7.2 is shown and the flow stops here — the avatar is unchanged and the form does not become dirty.
- On success, the Crop Image modal opens with a fixed 1:1 crop frame; the trader may pan and zoom the image within the frame (no rotate).
- The trader clicks [Done] → the photo is committed immediately, with no involvement from [Save Changes]:
- The modal closes and the blocking loading overlay Ref: OV-09 — "Updating avatar..." — covers the screen.
- The frontend requests a signed upload URL from the backend and
PUTs the cropped binary directly to S3 (Ref: BR_4.6.2.6). - The frontend calls
PATCH /profile-photowith{user_id, profile_photo_url}— a photo-only endpoint; any phone edit currently staged in the form is not sent and is not affected (Ref: BR_4.6.2.15). - On HTTP 200 the backend hard-deletes the previously stored S3 object (Ref: BR_4.6.2.6).
- The overlay closes, the new avatar is rendered on this screen and on every other avatar surface (Ref: BR_4.6.2.13), and the success toast Ref: TS-DASH-01 — "Changes saved successfully" — is shown (Ref: BR_4.6.2.16). [Remove Photo] becomes visible if it was hidden.
- The form's dirty state is untouched — if the trader had not edited the phone, [Save Changes] stays Disabled and [Cancel] stays hidden (Ref: BR_4.6.2.14).
- The trader clicks [Cancel] instead → the modal closes and the selection is discarded. Nothing is uploaded, the previously displayed avatar is retained, and the form's dirty state is left exactly as it was before the modal opened.
Alternative Flow B — Remove Photo
- [Remove Photo] is rendered only when the trader currently has a photo (
profile_photo_url IS NOT NULL); it is hidden entirely otherwise. - The trader clicks [Remove Photo] → the confirmation popup Ref: CF-03 is shown. Because the removal commits immediately and is not reversible via [Cancel], an explicit confirmation is required (BA decision, 2026-08-21). The popup shows the title "Remove your photo?" with no body text, and two vertically stacked buttons: [Confirm] on top (Primary), [Cancel] below (Secondary).
- The trader clicks [Cancel] in the popup → the popup closes and nothing happens; the avatar and the form's dirty state are unchanged.
- The trader clicks [Confirm] → the removal is committed immediately:
- The blocking loading overlay Ref: OV-09 covers the screen.
- The frontend calls
PATCH /profile-photowith{user_id, profile_photo_url: null}— the photo only; a staged phone edit is not sent (Ref: BR_4.6.2.15). - On HTTP 200 the backend hard-deletes the stored S3 object (Ref: BR_4.6.2.6).
- The overlay closes, the avatar reverts to the trader's initials (Ref: BR_4.6.2.7) on this screen and every other avatar surface (Ref: BR_4.6.2.13), the success toast Ref: TS-DASH-01 is shown (Ref: BR_4.6.2.16), and [Remove Photo] is hidden.
- The form's dirty state is untouched (Ref: BR_4.6.2.14).
- The removal cannot be undone from the UI — the previous image is gone from S3 and the trader must upload a new one.
7. Exceptional Flow
| # | Condition | System behaviour |
|---|---|---|
| 1 | GET /profile fails (network error or HTTP 5xx) at §5 step 2 | The General section does not render its values. Error toast Ref: TE-SYS-01. |
| 2 | Signed-URL request or the S3 PUT fails at §6 Alternative Flow A step 4.2 | The photo commit is aborted: PATCH /profile-photo is not called, the overlay Ref: OV-09 closes, and the previous avatar is restored — the cropped result is discarded, not held as a preview. Error toast: "Failed to upload file. Please try again later" Ref: CR-07 §7.2. The old S3 object is untouched. Retry = re-run [Change Photo] from the start. The form's dirty state and any staged phone edit are unaffected. |
| 3 | The signed URL expires between issuance and the S3 PUT | The frontend requests a fresh signed URL and retries the PUT once. If the retry also fails, behaviour is identical to row 2. |
| 4 | PATCH /profile-photo fails (network error or HTTP 5xx) at §6 Alternative Flow A step 4.3 or §6 Alternative Flow B step 4.2 | The overlay closes, the previous avatar is restored, and error toast Ref: TE-SYS-01 is shown. profile_photo_url is not written and no S3 object is deleted — the newly uploaded object may be left orphaned and is cleaned up backend-side. The form's dirty state and any staged phone edit are unaffected. |
| 5 | PATCH /profile returns HTTP 409 (phone taken by another account between the out-click check and submit) | Inline error Ref: IN-06 — "This phone number is already registered to another account.", the same string the checkout block uses — is rendered under the Phone field, [Save Changes] is Disabled, and the form stays dirty. The backend is the authority on uniqueness — the out-click check is only the first line of defence (Ref: BR_4.6.2.4). |
| 6 | PATCH /profile fails (network error or HTTP 5xx) at §5 step 8 | Error toast Ref: TE-SYS-01. phone_number is not persisted, the form keeps the entered value and stays dirty. Any photo already committed earlier in the session is not rolled back — the two paths are independent (Ref: BR_4.6.2.14). |
| 7 | Session token expired (HTTP 401) on any call in this UC | Error toast Ref: TE-AUTH-01 → redirect to the Auth0 login. Unsaved changes are lost. |
8. Business Rules
BR_4.6.2.1: Screen Entry Point and Tab Context
The Account screen is reached only through the sidebar footer user chip → dropdown → Settings (Ref: UC_4.1.2 §2 Screen Description, row 6). The chip renders the trader's avatar plus their current level — collapsed (avatar over Lvl 5) or expanded (avatar · Full Name · Level 15). The avatar is the saved profile photo, falling back to initials when none is set (Ref: BR_4.6.2.7, BR_4.6.2.13).
On entry, the default active tab is Connections & Credentials — the first entry in the tab list. Profile & Preferences becomes active only when the trader selects it. The full tab list and its owning UCs:
| Tab | Owning UC |
|---|---|
| Connections & Credentials | UC_4.6.1 — also the default active tab on entry |
| Market Data Management | UC_4.17.1 — Market Data Management (not yet documented in this repo). LIVE-only screen — hidden or rendered read-only for any trader in the simulation phase (Source: QnA from clients — Settings, Market Data Management row, Adrian Stack 2026-08-10). |
| Profile & Preferences | UC_4.6.2 (this document) |
| Billing | UC_4.6.3 — Invoice History table only; no billing portal and no third party (Ref: BR_4.6.3.2). Unlike Market Data Management, this tab is available to SIM and LIVE alike. |
| Account Actions | Not a single UC — the tab hosts two independent sections, each owned separately and each split by environment:- Reset Associate Track → UC_4.11.1 (SIM). No LIVE counterpart — a LIVE failure follows the Re-Buy path (UC_4.15.3, not yet documented in this repo).- Resign & Close Account → UC_4.11.2 (SIM) · UC_4.17.2 (LIVE, not yet documented in this repo). |
Note on the
UC_4.6.xrange: the WBS assigns onlyUC_4.6.1–UC_4.6.4toSTAGE 2: DASHBOARD - COMMON/ Settings, andUC_4.6.4is Controls (AI chat & Reduce motion) (SRS published) — Ref: BR_4.6.2.11. There is noUC_4.6.5, and Account Actions no longer sits in the common range at all. The containing folder has been renamedUC_4.6.1-4.6.5→UC_4.6.1-4.6.4to match the WBS; every link in the repo was remapped accordingly.
BR_4.6.2.2: Field Editability Matrix
The General section exposes exactly the three items below. No other profile attribute is rendered on this screen.
| Item | Editable | Rationale |
|---|---|---|
| Profile photo | Yes — upload (with crop) and remove | Source: QnA from clients — Settings, Profile & Preferences Definition row ("Photo (avatar upload - editable)") |
| No — read-only | It is the account's unique identity key, bound to the trading-gateway account and to the blacklist at checkout time. Changing it post-checkout carries data-integrity risk for little value, so it is locked for launch (Source: QnA from clients — Settings, Profile & Preferences Business logic row, Adrian Stack). Email is therefore never sent to either write endpoint, even though the RFQ's endpoint signature lists it as an optional parameter (Ref: BR_4.6.2.15). | |
| Phone | Yes | Source: QnA from clients — Settings, Profile & Preferences Definition + Business logic rows ("Changing Phone Number is ok", Adrian Stack) |
Full Name is likewise immutable, and is not rendered on this screen at all. It is still read from the Users record to derive the avatar initials (Ref: BR_4.6.2.7).
BR_4.6.2.3: Phone Format Validation — Reuses the Checkout Block
The phone field reuses the checkout validation block verbatim: two-part input (dial code selector, default +1, scroll-only country list) + digits-only number input, E.164 length of 6–15 digits excluding the dial code, leading zeros auto-trimmed on out-click, validated on out-click and on submit. Ref: CR-09 §9.2.
There is no geo-blocking by region on this field — any country dial code is accepted (Source: QnA from clients — Settings, Profile & Preferences Definition row).
There is no deviation from CR-09 §9.2 — including the error copy. The field renders the shared strings verbatim; Settings has no phone strings of its own (BA instruction, 2026-08-24):
| Situation | Timing | Error string | Source |
|---|---|---|---|
| Number input left empty | Out-click / Submit | "Please enter phone number" | CR-09 §9.2 |
| Invalid length or format (fails the digits-only / 6–15-digit E.164 check) | Out-click / Submit | "Please enter a valid phone number" | CR-09 §9.2 |
| Already registered to another account | Out-click / Submit | "This phone number is already registered to another account." | Ref: IN-06 — the checkout block's string (Ref: BR_4.6.2.4) |
| Identical to the currently saved number | Out-click / Submit | "The new phone number cannot be the same as your current one" | Ref: IN-10 — the only Settings-specific string, because the checkout field has no saved value to compare against (Ref: BR_4.6.2.5) |
This reverses the 2026-08-11 decision recorded in
QnA_init_docs.mdA-09 / A-10 / A-12, which created the Settings-only stringsIN-08andIN-09. Both are now retired in list-toast-popup.md v3.13 and must not be referenced.IN-08's copy — "Invalid phone number format. Please check and try again" — is no longer used anywhere in the system.CR-09 §9.2itself is unchanged: this UC conforms to it rather than editing it, so UC_2.8.3 (checkout) is behaviourally unaffected (BA instruction, 2026-08-24).
BR_4.6.2.4: Phone Uniqueness Across the System
A phone number may be associated with only one account. The check is the same one the checkout flow applies (Source: QnA from clients — Settings, Profile & Preferences Business logic row — Q: "Should we apply the same phone number validation logic here as well?" → A: "Yes").
- Trigger: phone field out-click, and again on submit.
- Authority: the backend is the final arbiter. The out-click check is only the first line of defence — a number that passes on out-click can still be claimed by another account before the trader clicks [Save Changes], in which case
PATCH /profilereturns HTTP 409 (Ref: §7 row 5). - Error display: inline error Ref: IN-06 under the Phone field — "This phone number is already registered to another account."; [Save Changes] stays Disabled until the value is edited to one that clears the check.
This is the same rule and the same string as the checkout rule BR_2.8.3.4: Phone Number Uniqueness (Ref: UC_2.8.3 §6) — both render Ref: IN-06. The Settings-only duplicate string IN-09 is retired (Ref: BR_4.6.2.3). Checkout behaviour is unchanged; only IN-06's trigger description was widened to name both screens.
BR_4.6.2.5: New Phone Number Must Differ From the Current One
The submitted phone number must not be identical to the one currently stored on the account.
- Comparison basis: the fully normalised E.164 string — dial code plus number, with all display formatting characters stripped. Changing only the dial code while keeping the same digits (e.g.
+1 5551234567→+44 5551234567) therefore counts as a valid change, not a duplicate. - Where enforced: client-side, on out-click and on submit — no backend round trip is required.
- Error display: inline error Ref: IN-10 — "The new phone number cannot be the same as your current one". [Save Changes] stays Disabled until the value differs.
- Why this one string stays Settings-specific: the checkout phone field is filled for the first time and has no "currently saved" value, so neither CR-09 §9.2 nor the checkout block defines a counterpart to reuse (Ref: BR_4.6.2.3).
BR_4.6.2.6: Profile Photo — Upload Constraints, S3 Signed-URL Flow, and Old-Object Deletion
Upload constraints (the values CR-07 §7.1 requires this UC to specify):
| Criterion | Value |
|---|---|
| Maximum file count | 1 |
| Allowed formats | .jpg, .jpeg, .png, .webp |
| Max size per file | 4 MB |
Both constraints are enforced client-side before the Crop Image modal opens; violations use the standard messages in Ref: CR-07 §7.2.
Upload mechanics — the image never transits the application backend (Source: RFQ_ Stack Trading Prop Tech V7.pdf §Page 36 — "Supports image uploads via S3 signed URL execution before submission"):
- The frontend requests a short-lived signed upload URL from the backend.
- The frontend
PUTs the cropped image binary directly to S3 using that URL. - The resulting S3 object URL is sent as
profile_photo_urltoPATCH /profile-photo— a photo-only endpoint that carries no other field (Ref: BR_4.6.2.15).
When these steps run. Steps 1–3 fire the moment the trader clicks [Done] in the Crop Image modal — not on [Save Changes] (Ref: §6 Alternative Flow A step 4, BR_4.6.2.14). The whole sequence runs behind the blocking overlay Ref: OV-09.
On "upload and submission are one step". The RFQ describes the upload as running "before submission" — i.e. as a preparatory step ahead of a separate, trader-initiated save. That separation no longer exists: [Done] is the only trader action, and steps 1–3 run as one uninterruptible sequence behind
OV-09with no intermediate state the trader can see, cancel, or leave un-submitted. The three network hops are an internal implementation detail of a single logical operation. The signed-URL mechanism itself is retained — the image binary still never transits the application backend (BE team decision, 2026-08-21 — Ref: BR_4.6.2.15).
Old-object deletion. A trader has exactly one profile image at any time; the system keeps no history. Once the PATCH /profile-photo write returns HTTP 200, the backend hard-deletes the previously stored S3 object in the same operation — on replacement (Alternative Flow A) and on removal (Alternative Flow B) alike (BA instruction, 2026-08-21). Consequences:
- Deletion is conditional on the write succeeding. If the upload or the write fails, the old object is left intact and remains the trader's avatar (Ref: §7 rows 2 and 4).
- A replaced or removed photo is unrecoverable from the UI. There is no undo and no [Cancel] path — the trader must upload the image again. This is why [Remove Photo] is gated by the confirmation popup Ref: CF-03.
- An object uploaded to S3 for a commit whose
PATCH /profile-photothen failed is orphaned; reclaiming it is a backend housekeeping concern with no user-facing effect and is out of scope for this UC.
BR_4.6.2.7: Avatar Fallback to Initials
Whenever profile_photo_url IS NULL — a brand-new account that never uploaded a photo, or an account whose photo was removed — the avatar renders the trader's initials derived from the Full Name held in the Users record. It never renders a generic placeholder figure (BA instruction, 2026-08-11 — Ref: QnA_init_docs.md A-04).
Derivation rule, applied to the Full Name captured at checkout:
| Case | Result | Example |
|---|---|---|
| Two or more words | First letter of the first word + first letter of the last word, uppercased | Taylor Swift → TS; Nguyen Thi Quynh Anh → NA |
| Exactly one word | First letter of that word, uppercased | Madonna → M |
| Full Name empty or null | First letter of the email local-part, uppercased | trader@mail.com → T |
The same derivation applies on every surface that renders a trader avatar, not just this screen — see BR_4.6.2.13 for the full consumer list. It is applied per displayed user, so a POD member with no photo of their own shows their own initials (e.g. Marcus Chen → MC) regardless of whether the viewing trader has a photo.
BR_4.6.2.8: Dirty-State Control of [Save Changes] and [Cancel]
The Phone field is the only input that makes this form dirty. The dial code and the phone number are the sole participants; the profile photo is not, because it commits on its own (Ref: BR_4.6.2.14).
| Form state | [Cancel] | [Save Changes] |
|---|---|---|
| Clean — phone unchanged since load or since the last successful save | Hidden | Disabled |
| Dirty (phone edited), no inline error active | Visible | Enabled |
| Dirty (phone edited), at least one inline error active (a CR-09 §9.2 empty/format error, IN-06, or IN-10 — Ref: BR_4.6.2.3) | Visible | Disabled |
| Photo changed or removed, phone untouched | Hidden | Disabled |
Clicking [Cancel] reverts the staged phone value and dial code back to the last successfully saved state and returns the form to clean. It does not — and cannot — revert a photo change, which was already persisted at the moment it was made. No confirmation popup is shown, because nothing about the phone has been persisted yet.
A change to the Controls panel toggles never affects this dirty state either (Ref: BR_4.6.2.11).
BR_4.6.2.9: Unsaved-Changes Navigation Warning
While the form is dirty — i.e. the phone has been edited (Ref: BR_4.6.2.8) — the trader is warned before their staged changes would be discarded. A committed photo change never triggers this warning, because there is nothing staged left to lose (Ref: BR_4.6.2.14):
- Switching to another Settings tab, or navigating to another sidebar destination → confirmation popup Ref: CF-01, per CR-05. Choosing Leave discards the staged changes and completes the navigation; choosing Cancel keeps the trader on the tab with the form untouched.
- Closing or reloading the browser tab → the browser's own native unload prompt. Its copy is controlled by the browser and cannot be customised, so CF-01 does not apply to this case.
BR_4.6.2.10: Profile Is Global Per User — No SIM/LIVE Branching
This screen behaves identically for SIM (Evaluation) and LIVE (funded) accounts: same fields, same editability, same validation, same endpoints. The profile is a property of the user, not of the trading environment (Source: QnA from clients — Settings, Profile & Preferences Definition row, LIVE column: "Same as SIM - the profile is global per user, no SIM/LIVE distinction").
Consequently no rule in this UC is gated on account status, level, or environment.
BR_4.6.2.11: Controls Panel Is Out of Scope
The Controls panel (Desk Manager and Reduce Motion toggles) is rendered next to the tab list on every Settings tab, including this one, but it is not owned by this UC. It belongs to UC_4.6.4 — Controls (AI chat & Reduce motion). The Desk Manager toggle writes through its own dedicated endpoint POST /ai-preference; the Reduce Motion toggle writes nothing to the backend at all — it is frontend-only, persisted per device, and POST /accessibility-preference is withdrawn (client answer 2026-08-24, Ref: BR_4.6.4.2).
Two consequences for this UC:
- Toggling either switch saves immediately on flip. It does not route through this screen's [Save Changes] button and does not mark this form dirty (Ref: BR_4.6.2.8).
- The
ui_preferencesobject returned byGET /profileis not read by this screen, andui_preferencesis never sent to either of this screen's write endpoints — even though the RFQ endpoint signature lists it as an optional parameter (BA decision, 2026-08-11 — Ref:QnA_init_docs.mdA-18). This boundary is unchanged by the 2026-08-24 client answer; note only thatui_preferencesno longer has a consumer in UC_4.6.4 either, since Reduce Motion left the database and the Desk Manager uses the dedicatedusers.ai_enabledcolumn. ✅ Re-confirmed 2026-09-03 — the client states the field has "no active parameters to parse… today" and exists "strictly as a future-proofing measure", so having no reader and no writer is the intended state, not an omission (Ref: BR_4.6.2.15,[CTRL-EP-05]).
BR_4.6.2.12: Change Frequency and Downstream Effects of a Phone Change
For launch, a phone number change is a plain field update with no additional ceremony:
- No rate limit and no cool-down — the trader may change their phone number any number of times. Confirmed for launch 2026-08-27 ("ở bản launch, trader có thể thay đổi số điện thoại tự do mà không bị khóa thời gian"). Note the status of the source: the client's QnA row is still marked "STILL OPEN: no rate limit / cool-down was specified" — so this is a build decision taken in the absence of a client limit, not a limit the client has ruled out. If they later specify one, it lands here.
- No security constraint of any kind — confirmed for launch 2026-08-27. A phone change passes exactly two gates and is then written straight to PostgreSQL: the checkout regex format check (Ref: BR_4.6.2.3) and the system-wide uniqueness check (Ref: BR_4.6.2.4). Nothing else stands between the trader and the write.
- No OTP verification of the new number. Dashboard authentication is Auth0 passwordless over email, so the phone number is not currently an authentication factor.
- No security-notification email to the previous address, and no audit-log entry beyond the Users record's own
updated_attimestamp. - No propagation to Auth0, Rithmic, MT5, or TraderEvolution — the number lives only in the StackTrading Users table.
These four points define launch scope only. The phone number was originally collected at checkout to "enable Two-Factor Authentication (2FA) and coordinate emergency Drawdown Defense protocols" (Ref: UC_2.8.3 §7). If either capability is activated later, an unverified self-service phone change becomes an account-takeover vector and this rule must be revisited.
BR_4.6.2.13: Avatar Propagation — Consumer Surfaces
The photo saved on this screen is the single source for the trader's avatar across the whole Dashboard. There is no second avatar setting anywhere else (BA instruction, 2026-08-11 — Ref: QnA_init_docs.md A-20). Once PATCH /profile-photo returns HTTP 200, the new profile_photo_url is what every consumer surface below renders.
| Consumer surface | What it renders | Owning UC |
|---|---|---|
| Sidebar user chip — collapsed state | Avatar + level, stacked (e.g. avatar over Lvl 5). Before any photo is uploaded this is the initials block (e.g. SD over Lvl 5) | UC_4.1.2 |
| Sidebar user chip — expanded state | Avatar + Full Name + level on one row (e.g. photo · Sam Smith · Level 15) | UC_4.1.2 |
My POD widget — Pod Leader card, Community page (LIVE) | Leader's avatar + name + title and level (e.g. Marcus Chen · Senior trader - Lvl 12) | UC_4.14.2 — Pod Mentorship Module (LIVE, not yet documented in this repo) |
My POD widget — POD members list, Community page (LIVE) | One avatar per member + name + track and level (e.g. Cameron Williamson · Associate - Lvl 3) | UC_4.14.2 — Pod Mentorship Module (LIVE, not yet documented in this repo) |
Mechanics of the propagation:
- Read-through, not a push. Each surface reads the
profile_photo_urlof the user it is displaying, straight from the Users record. This UC writes one column and pushes nothing — there is no sync job, no fan-out event, and no per-surface copy of the image to keep consistent. - Fallback is per displayed user. Any surface showing a user whose
profile_photo_url IS NULLrenders that user's own initials per BR_4.6.2.7 — which is why POD member rows can show initials (MC) and a photo side by side in the same list. - Refresh timing. The sidebar user chip refreshes immediately on the photo commit's HTTP 200 (Ref: §6 Alternative Flow A step 4.5 / Alternative Flow B step 4.4), because it is part of the shell the trader is already on. Every other surface picks the new value up on its own next data fetch; this UC does not force a re-render of screens the trader is not currently viewing.
- Level and name are not owned here. The level (
Lvl 5,Level 15), the title/track label (Senior trader,Associate), and the Full Name shown next to the avatar all come from each consumer surface's own data contract. This UC contributes the image only.
Dashboard avatar vs. Discord avatar — closed 2026-08-27. The two are fully independent; Discord is never read.
The precedence question is resolved by removing the contest: StackTrading never pulls an avatar or a name from the Discord API, on any surface, as a primary value or as a fallback.
| Surface | Avatar shown |
|---|---|
Web Dashboard — this screen, the sidebar user chip, the My POD widget | The photo the trader uploaded here, or their initials when profile_photo_url IS NULL (Ref: BR_4.6.2.7). Nothing else is ever consulted. |
| Discord server | Whatever the trader has set on their own Discord account — except for a chat message sent from the LIVE Dashboard, which StackTrading posts through a Discord Webhook carrying the trader's profile_photo_url (Ref: the amendment below). |
- Rationale, stated by the client: pulling Discord's CDN image invites broken images and expired CDN links — Discord avatar URLs are not stable third-party assets, and a dead link on the sidebar chip or a POD member row would be a visible defect StackTrading could not fix.
- A trader will therefore often show two different pictures — one on the Dashboard, one in Discord — and that is the intended outcome, not a sync gap to be closed later.
- Consequence for UC_4.14.2 (Pod Mentorship, LIVE): the
My PODwidget's data contract is now constrained, not open. It rendersprofile_photo_urlwith an initials fallback like every other surface; a Discord-sourced secondary fallback is excluded, so that branch does not need designing there. Adding one would be a change request against this rule. - The same rule applies to names: the Full Name beside an avatar comes from the Users record, never from a Discord nickname or global display name (Ref: BR_4.5.2.6 — StackTrading does not manage Discord display names).
⚠️ Amendment — CR-20260903-001, 2026-09-03. The one-way boundary above is no longer absolute in the Discord direction.
Live-broadcast chat messages sent from the Dashboard now leave through a Discord Webhook that overrides
usernameandavatar_urlper payload, so that they render in Discord as the trader rather than as a bot transcript. Two clauses of this BR are affected:
Clause as closed 2026-08-27 Status after the CR "StackTrading neither reads nor influences" the Discord surface 🔴 Superseded. profile_photo_urlis now pushed to Discord on every Dashboard-sent chat message (Ref: BR_4.14.1.12)"StackTrading never pulls an avatar or a name from the Discord API, on any surface, as a primary value or as a fallback" 🔴 Superseded in principle — the CR's avatar priority 2 (Discord photo when profile_photo_url IS NULL) requires exactly such a read. ⚠️ Not yet confirmed by the client as a deliberate reversal, and the 2026-08-27 rationale ("expired CDN links... a dead link would be a visible defect") still stands technically. Tracked as UC_4.14.1 §10 item 12Everything governing Dashboard surfaces — this screen, the sidebar chip, My POD, the initials fallback of BR_4.6.2.7, and theMy PODconstraint on UC_4.14.2🟢 Unchanged. No Dashboard surface reads Discord, and none renders a chat avatar New constraint this creates on
PATCH /profile-photo: the storedprofile_photo_urlmust be publicly fetchable by Discord's servers, since Discord retrievesavatar_urlitself. A private bucket or a signed URL with a TTL silently degrades every trader to the webhook's default image. BE confirmation pending — UC_4.14.1 §10 item 13.
BR_4.6.2.14: Photo and Phone Are Two Independent Commit Paths
The General section looks like one form but behaves as two unrelated transactions. This is the governing rule of the screen; BR_4.6.2.6 and BR_4.6.2.8 are its consequences (BA instruction, 2026-08-21).
| Profile photo | Phone | |
|---|---|---|
| Commit trigger | Crop Image [Done], or [Confirm] in Ref: CF-03 | [Save Changes] |
| Endpoint | PATCH /profile-photo {user_id, profile_photo_url} | PATCH /profile {user_id, phone_number} |
| Marks the form dirty? | No | Yes |
| Enables [Save Changes]? | No | Yes |
| Revertible via [Cancel]? | No — already persisted | Yes — while uncommitted |
| Progress feedback | Blocking overlay Ref: OV-09 during the commit, then Ref: TS-DASH-01 toast on success (Ref: BR_4.6.2.16) | Ref: TS-DASH-01 toast on success |
Rules that follow from this split:
- A photo-only session never lights up [Save Changes]. If the trader opens the tab, changes their avatar and changes nothing else, [Save Changes] stays Disabled and [Cancel] stays hidden for the whole session — there is nothing left to save.
- The two payloads never merge. They cannot — they are different endpoints (Ref: BR_4.6.2.15).
profile_photo_urlis never sent by [Save Changes], andphone_numberis never sent by a photo commit. A phone value staged in the form is simply not part of the photo call, and survives it untouched. - A failure on one path does not roll back the other (Ref: §7 rows 4 and 6).
- Order does not matter. The trader may commit a photo before, after, or in the middle of editing the phone; each path is evaluated on its own.
- The unsaved-changes guard covers the phone only. Navigating away with a committed photo but an untouched phone triggers no warning, because the form is clean (Ref: BR_4.6.2.9).
BR_4.6.2.15: API Contract — Endpoint Split and Verb
This screen talks to three endpoints. The write side deliberately deviates from the single Update User Profile operation described in RFQ_ Stack Trading Prop Tech V7.pdf §Page 36 (Sotatek BE team decision, API review 2026-08-21).
| Endpoint | Verb | Payload | Called from | Response |
|---|---|---|---|---|
/profile | GET | user_id | §5 step 2 — screen load | {profile_photo_url, email, phone_number, ui_preferences} |
/profile | PATCH | {user_id, phone_number} | [Save Changes] — §5 step 8 | HTTP 200 · 409 on duplicate phone |
/profile-photo | PATCH | {user_id, profile_photo_url} — the S3 object URL, or null to remove | Crop Image [Done] — §6 Alt Flow A step 4.3 · [Confirm] in CF-03 — §6 Alt Flow B step 4.2 | HTTP 200 |
The three deviations from the RFQ, and why:
| # | RFQ_ Stack Trading Prop Tech V7.pdf §Page 36 | This document | Rationale |
|---|---|---|---|
| 1 | POST Update User Profile | PATCH on both write endpoints | The operation is a partial update of an existing resource, never a creation. PATCH is the correct verb; POST was imprecise. |
| 2 | "Supports image uploads via S3 signed URL execution before submission" | Upload and submission are one operation | The RFQ wording assumes the trader submits the form separately after the upload has been prepared. That step no longer exists — [Done] is the only trader action and the whole chain runs behind OV-09 (Ref: BR_4.6.2.6, BR_4.6.2.14). The signed-URL mechanism itself is unchanged — the binary still never transits the application backend. |
| 3 | One endpoint writes profile_photo_url, email, phone_number and ui_preferences together | Two endpoints, one field group each | The photo and the phone commit on different triggers at different times (Ref: BR_4.6.2.14), so a shared endpoint would need every field optional and could not express "photo only" vs "phone only" cleanly. email is read-only (Ref: BR_4.6.2.2) and ui_preferences belongs to UC_4.6.4 (Ref: BR_4.6.2.11), so neither is written here at all. |
Naming. Singular /profile and /profile-photo — this is a singleton resource scoped to the authenticated trader, so the plural form would be wrong. GET /profile keeps its existing name and shape; UC_4.6.4 also reads from it and is unaffected by this change.
✅
[PP-EP-01]closed 2026-08-27. The BE team's proposal is adopted in full — "Chuyển đổi toàn bộ sang chuẩn RESTful sử dụngPATCH /profile" and "Nhất trí với quyết định phân tách này". All three deviations above are confirmed as the built behaviour:PATCHon both write endpoints, the two-endpoint split, and the merged crop → signed URL → S3 →PATCH /profile-photochain behind a single [Done]. The competing names are formally retired: the WBS'sPUT /profileand the RFQ'sPOST Update User Profileare both superseded and must not be cited as the contract. WBS row 174 still carriesPUT /profileand needs correcting.
✅
ui_preferences— discrepancy resolved the same day; the payload isphone_numberonly. An earlier answer on 2026-08-27 had describedPATCH /profileas updating "số điện thoại (phone_number) và cấu hình giao diện (ui_preferences)". The follow-up answer on UC_4.6.4 settles it the other way: because the Desk Manager keeps its own dedicated endpoint ("POST /ai-preference— không gộp chung vào profile nữa"),PATCH /profileis reduced to "Chỉ dùng để cập nhật số điện thoại" with the payload given as{"phone_number": "+84901234567"}. This document's payload therefore stands, for two reasons that both post-date the field's last live use:
- A-18 (2026-08-11) drew the boundary explicitly: UC_4.6.2 does not read or write
ui_preferences; it belongs to UC_4.6.4 (Ref: BR_4.6.2.11).- The client answer of 2026-08-24 left the column with no consumer at all.
Reduce Motionis stored locally per device and never server-side;Desk Manageruses its own booleanusers.ai_enabledviaPOST /ai-preference. Soui_preferencesis not written by UC_4.6.4 either — writing it from this screen would resurrect a dead field and give it a writer but still no reader.The column remains in the Zapier Users schema and in the
GET /profileresponse shape, which is unchanged.ui_preferencesnow has neither a reader nor a writer anywhere in the Settings module — it is a dormant column, not a live field.✅ Confirmed by the client on 2026-09-03 — the column is dormant by design, and this is not a documentation gap. Asked directly what the field is for, the client answered: "There are currently no active parameters to parse for this field today. It is intentionally included in the schema strictly as a future-proofing measure. Please maintain this column as an optional JSON object. This ensures the frontend team can easily save arbitrary cross-device layout preferences in future updates without requiring structural database schema changes later." (Source:
[CTRL-EP-05], QnA STAGE 2 — Settings §9 Controls & Concierge.) Three build consequences:
- Keep the column and keep it in the contract.
users.ui_preferencesis JSONB, nullable / optional.GET /profilereturns it as-is; a write endpoint that accepts it treats it as an optional pass-through with no validation of its inner shape. Nothing on the backend parses it in this release.- This screen still never sends it. The payload of
PATCH /profilestays{user_id, phone_number}— the client's own answer scopes that endpoint to the phone number, and the field has no reader to serve.- The earlier "future use = change request" note is superseded. Adding a frontend-only, cross-device layout key later is explicitly pre-authorised by this answer — no DB migration and no CR. A key that carries backend logic or cost (anything a worker or a Zapier flow must read) is a different matter and does need a CR, because it makes the column live.
✅
user_idstays in the payload — confirmed 2026-08-28. The 2026-08-27 payload examples omitted it ({"phone_number": …},{"profile_photo_url": …}) whilePOST /ai-preferencein the same answer carried it. The inconsistency is resolved in favour of keepinguser_idon all three write endpoints —PATCH /profile,PATCH /profile-photoandPOST /ai-preference— "để đảm bảo tính nhất quán cao nhất với tài liệu đặc tả RFQ gốc". The payload table above stands as written; the omission in those examples was shorthand, not a contract change.This is a consistency decision, not an authorisation one: the endpoints remain authenticated, and
user_idin the body must never be trusted as the authority for whose record is written. BE resolves the acting trader from the session and rejects a mismatch — a payloaduser_idthat differs from the authenticated trader is an error, never an instruction to write someone else's row.
BR_4.6.2.16: Photo Commit — Success Confirmation Toast
A profile-photo commit ends with the same success toast as every other Settings save: Ref: TS-DASH-01 — "Changes saved successfully" (BA instruction, 2026-08-28).
- When it fires. On HTTP 200 from
PATCH /profile-photo— on both commit triggers: Crop Image [Done] (Ref: §6 Alternative Flow A step 4.5) and [Confirm] in Ref: CF-03 (Ref: §6 Alternative Flow B step 4.4). The two triggers share one endpoint and one commit model, so they share one confirmation. - Sequence. The blocking overlay Ref: OV-09 closes first, then the toast is raised. The two are never on screen together:
OV-09reports work in progress, the toast reports work finished. - Copy. Identical to the phone save — the string is not specialised per field.
TS-DASH-01is the single Settings-wide success toast (Ref: BR_4.6.2.14). - On failure, no success toast is shown. The failure branches raise their own error toast and restore the previous avatar (Ref: §7 rows 2 and 4).
- This does not change the commit model. The toast is confirmation only: the photo path still commits on its own trigger, still never marks the form dirty, and still never involves [Save Changes] (Ref: BR_4.6.2.14).
9. Screen Description
9.1 Account screen — Profile & Preferences tab
| No. | Field Name | Field Type | Displaying rule / Behaviour rule |
|---|---|---|---|
| 1 | Screen title | Label | Displaying rule:- Static text: "Account". Constant across all Settings tabs. |
| 2 | Settings tab list | Tab | Displaying rule:- Five tabs in fixed order: Connections & Credentials · Market Data Management · Profile & Preferences · Billing · Account Actions. Default active tab on entry = Connections & Credentials. Exactly one tab active at a time. Tab → owning UC mapping: Ref: BR_4.6.2.1.Behaviour rule:- On click: switches the right-hand content pane to that tab. If this form is dirty, the switch is intercepted by Ref: CF-01 first (Ref: BR_4.6.2.9). |
| 3 | Controls panel | Toggle/Switch | Displaying rule:- Persistent panel below the tab list, containing the Desk Manager and Reduce Motion toggles. Rendered on this tab but owned by UC_4.6.4 — Ref: BR_4.6.2.11.Behaviour rule:- Each toggle saves immediately on flip via its own endpoint. Does not participate in this screen's [Save Changes] / dirty-state logic. |
| 4 | Section header | Label | Displaying rule:- Static text: "General". |
| 5 | Profile photo | Label | Displaying rule:- Read-only avatar display. Shows the image at profile_photo_url when set; otherwise the trader's initials Ref: BR_4.6.2.7.- There is no "staged preview" state — the value shown is always the persisted one, because every photo change commits on the spot Ref: BR_4.6.2.14.Behaviour rule:- Not clickable — the photo is changed only through the two buttons below it. |
| 6 | [Change Photo] | Button (Secondary) | Displaying rule:- Always Enabled.Behaviour rule:- On click: opens the native file picker, then the Crop Image modal on a valid selection — Ref: §6 Alternative Flow A.Impact:- Does not mark the form dirty and does not enable [Save Changes] at any point in the flow — the commit happens inside the modal Ref: BR_4.6.2.14. |
| 7 | [Remove Photo] | Button (Danger) | Displaying rule:- Rendered only when profile_photo_url IS NOT NULL; hidden entirely when the avatar is already showing initials — including immediately after a successful removal, without a page reload.Behaviour rule:- On click: opens the confirmation popup Ref: CF-03 — "Remove your photo?", buttons [Confirm] / [Cancel] stacked vertically. [Cancel] → nothing happens. [Confirm] → commits immediately via PATCH /profile-photo {user_id, profile_photo_url: null} behind overlay Ref: OV-09 — Ref: §6 Alternative Flow B.Impact:- Does not mark the form dirty and is not reversible via [Cancel] — Ref: BR_4.6.2.14. The stored S3 object is hard-deleted, so the previous image cannot be restored from the UI; the trader must upload it again. Ref: BR_4.6.2.6.- Every avatar surface reverts to the trader's initials, including the sidebar user chip and the My POD widget on the LIVE Community page. Ref: BR_4.6.2.13. |
| 8 | Upload hint | Label | Displaying rule:- Static text: "Pick a photo up to 4MB." Does not enumerate the accepted extensions; those are enforced silently and surfaced only on error — Ref: BR_4.6.2.6. |
| 9 | Textbox | Displaying rule:- Read-only — Ref: BR_4.6.2.2. Rendered inside a disabled input container; not focusable, not editable, no copy action, no clear action.- Value from GET /profile → email. Max 100 characters per CR-09 §9.1; overflow: truncate with tooltip.- Never sent to either write endpoint — Ref: BR_4.6.2.15. | |
| 10 | Phone — Country Code | Dropdown (Single-selection) | Displaying rule:- Ref: CR-09 §9.2. Default on first load: +1. Scroll-only country list, no search input. Populated from the stored number's dial code when one exists.Behaviour rule:- On change: marks the form dirty and re-runs the phone validation chain (Ref: §5 step 6). |
| 11 | Phone — Number | Textbox | Displaying rule:- Ref: CR-09 §9.2. Digits only, 6–15 digits excluding dial code, leading zeros auto-trimmed on out-click. Overflow: block input past the 15-digit limit.Validation: — all copy is the shared checkout / Common Rule copy; no Settings-specific string except IN-10. Ref: BR_4.6.2.3.- Empty → CR-09 §9.2: "Please enter phone number".- Invalid format or length → CR-09 §9.2: "Please enter a valid phone number".- Identical to the currently saved number → Ref: IN-10. Ref: BR_4.6.2.5.- Already registered to another account → Ref: IN-06 — the checkout string. Ref: BR_4.6.2.4.- Checks run on out-click and again on submit, in the order above; the first failure is the one displayed.- IN-08 / IN-09 are retired and must not be used.Behaviour rule:- On change: marks the form dirty. Any active inline error clears as soon as the value is edited.Impact:- Ref: BR_4.6.2.12 — no OTP verification, no notification email, no propagation to Auth0 / Rithmic / MT5 / TraderEvolution, and no change-frequency limit at launch. |
| 12 | [Cancel] | Button (Secondary) | Displaying rule:- Hidden while the form is clean; visible only while the phone is dirty — Ref: BR_4.6.2.8. A photo change alone never reveals it.Behaviour rule:- On click: reverts the phone value and dial code to the last saved state and returns the form to clean. No confirmation popup — nothing about the phone has been persisted.Impact:- Has no effect on the profile photo, which was already committed at the moment it was changed — Ref: BR_4.6.2.14. |
| 13 | [Save Changes] | Button (Primary) | Displaying rule:- Disabled while the form is clean or while any inline error is active; Enabled only when the phone is dirty and valid — Ref: BR_4.6.2.8.- Stays Disabled when the trader only changes or removes the photo — the photo is not part of this button's scope Ref: BR_4.6.2.14.Behaviour rule:- On click: calls PATCH /profile with {user_id, phone_number}. Never calls PATCH /profile-photo and never triggers an S3 upload — Ref: BR_4.6.2.15.- On HTTP 200: success toast Ref: TS-DASH-01; form returns to clean.- On failure: Ref: §7 Exceptional Flow.Impact:- Writes phone_number on the Users record. No other profile column is touched; profile_photo_url, email and ui_preferences are never sent by this button.- Ref: BR_4.6.2.12 — no OTP, no notification email, no propagation to Auth0 / Rithmic / MT5 / TraderEvolution. |
9.2 Crop Image modal
| No. | Field Name | Field Type | Displaying rule / Behaviour rule |
|---|---|---|---|
| 1 | Modal title | Label | Displaying rule:- Static text: "Crop Image". |
| 2 | Crop viewport | Label | Displaying rule:- Shows the selected image inside a fixed 1:1 crop frame. Opens only after the file passes the format and size checks in Ref: BR_4.6.2.6.Behaviour rule:- The trader may pan and zoom the image within the frame. Rotate is not supported. |
| 3 | [Done] | Button (Primary) | Behaviour rule:- On click: closes the modal and commits the photo immediately — overlay Ref: OV-09 → signed URL → PUT to S3 → PATCH /profile-photo {user_id, profile_photo_url} → backend hard-deletes the old S3 object. The three hops run as one uninterruptible sequence — there is no separate submission step. On HTTP 200 the overlay closes and the success toast Ref: TS-DASH-01 — "Changes saved successfully" — is shown. Ref: §6 Alternative Flow A, BR_4.6.2.6, BR_4.6.2.16.Impact:- Does not mark the form dirty and does not enable [Save Changes] — Ref: BR_4.6.2.14.- Not reversible: the previous image is deleted on success. |
| 4 | [Cancel] | Button (Secondary) | Behaviour rule:- On click: closes the modal and discards the selection. Nothing is uploaded, the previously displayed avatar is retained, and the form's dirty state is unchanged. |
9.3 Remove photo confirmation popup — Ref: CF-03
| No. | Field Name | Field Type | Displaying rule / Behaviour rule |
|---|---|---|---|
| 1 | Modal title | Label | Displaying rule:- Static text: "Remove your photo?". There is no body text — the title is the entire message. |
| 2 | [Confirm] | Button (Primary) | Displaying rule:- Top of the vertical button stack. Always Enabled while the popup is open.Behaviour rule:- On click: closes the popup and commits the removal — overlay Ref: OV-09 → PATCH /profile-photo {user_id, profile_photo_url: null} → backend hard-deletes the stored S3 object. On HTTP 200 the overlay closes and the success toast Ref: TS-DASH-01 is shown. Ref: §6 Alternative Flow B, BR_4.6.2.16.Impact:- Irreversible — no undo, and [Cancel] on the form behind cannot revert it. Ref: BR_4.6.2.6, BR_4.6.2.14.- Does not mark the form dirty and does not enable [Save Changes]. |
| 3 | [Cancel] | Button (Secondary) | Displaying rule:- Below [Confirm] in the stack.Behaviour rule:- On click: closes the popup. Nothing is called, the avatar is unchanged, and the form's dirty state is untouched. |
Layout note. This popup stacks its two buttons vertically with the affirmative action on top, unlike CF-01 / CF-02, which place them side by side with Cancel first. The design set is the authority here (BA design frame, 2026-08-21).
Changelog
| Date | Version | Updated item | Before | After | Notes |
|---|---|---|---|---|---|
| 2026-09-03 | v14 | BR_4.6.2.15 — ui_preferences reclassified from "dormant, re-introducing it is a CR" to "dormant by design, pre-authorised for future FE-only keys" · BR_4.6.2.11 · Document References row 3d (new) | The closing blockquote said the column "has neither a reader nor a writer anywhere in the Settings module" and that "if a future preference genuinely needs it, adding it to this payload is a change request". The document could not say why the field existed at all — it read as an unexplained leftover in the RFQ signature. | The client's own rationale is recorded: the field is "intentionally included in the schema strictly as a future-proofing measure" with "no active parameters to parse… today", and BE must "maintain this column as an optional JSON object" so the FE can later save "arbitrary cross-device layout preferences… without requiring structural database schema changes". Three build consequences spelled out: keep the JSONB column as an optional pass-through with no inner validation; this screen's payload is unchanged ({user_id, phone_number}); and a future FE-only layout key needs no CR and no migration — only a key carrying backend logic or cost still does. | No behavioural change to this UC — the payload, the endpoints and the boundary with UC_4.6.4 all stand exactly as in v13. This entry replaces a BA inference with the client's stated intent, and removes a false CR trigger that would otherwise have been raised during build. Source: client answer 2026-09-03 → [CTRL-EP-05] in QnA_STAGE2_SETTINGS_CAREER_VIRAL_EN.md §9. |
| 2026-09-03 | v13 | BR_4.6.2.13 — Dashboard vs. Discord avatar boundary | "StackTrading never pulls an avatar or a name from the Discord API, on any surface, as a primary value or as a fallback"; Discord surface = "neither reads it nor influences it" | Amendment block added: the influences clause is superseded — profile_photo_url is now pushed into the Discord Webhook payload for every chat message sent from the LIVE Dashboard; the never pulls clause is superseded in principle by avatar priority 2 (Discord photo as fallback), flagged as not yet confirmed as a deliberate reversal. All Dashboard surfaces, the initials fallback and the My POD constraint are unchanged | Applies CR-20260903-001. New constraint recorded on PATCH /profile-photo: the stored URL must be publicly fetchable by Discord's servers, otherwise every trader silently degrades to the webhook's default image. Two items pending: UC_4.14.1 §10 items 12 and 13 |
| 2026-08-28 | v12 | BR_4.6.2.16 (new) · §4 Post-conditions · §6 Alt Flow A step 4.5 · §6 Alt Flow B step 4.4 · BR_4.6.2.14 · §9.2 row 3 · §9.3 row 2 — photo commit now raises a success toast | A photo commit showed no success confirmation at all. The only feedback was the blocking overlay OV-09 disappearing and the avatar changing. TS-DASH-01 was explicitly scoped away from this path — its context line read "a photo commit uses OV-09 and fires no toast on success" — so a trader who clicked [Done] was never told the write had persisted. | On HTTP 200 from PATCH /profile-photo, the overlay closes and Ref: TS-DASH-01 — "Changes saved successfully" — is shown, exactly as on a phone save. Applies to both photo triggers: Crop Image [Done] and [Confirm] in CF-03. Overlay and toast are never on screen together. Failure branches are unchanged (previous avatar restored + error toast, no success toast). | BA instruction 2026-08-28. Parity with the phone save — the two commit paths differ in trigger and endpoint (BR_4.6.2.14), not in how success is reported. No new toast ID — TS-DASH-01 is reused, its context line widened in list-toast-popup.md v3.20. The commit model is untouched: the photo still never marks the form dirty. |
| 2026-08-27 | v11 | BR_4.6.2.15 — [PP-EP-01] closed · ui_preferences discrepancy recorded | The BR ended with a ❓ blockquote: [PP-EP-01] PENDING, the client had never confirmed an endpoint name, and the WBS (PUT /profile), the RFQ (POST Update User Profile) and this document gave three different answers. The BR was explicitly said not to close it. | ✅ Closed. The BE proposal is adopted in full — PATCH on both write endpoints, the two-endpoint split, and the merged crop → signed URL → S3 → PATCH /profile-photo chain. PUT /profile and POST Update User Profile are formally superseded and must not be cited as the contract; WBS row 174 flagged for correction. A second 🔴 blockquote records that the same answer described PATCH /profile as also writing ui_preferences — not adopted, with the two reasons why (A-18's boundary, and the field having had no consumer at all since 2026-08-24). Payload stays {user_id, phone_number}. | BA/client answers 2026-08-27, Phần II. Contract itself unchanged from v8 — this entry closes the open question and blocks a re-introduction during build. |
| 2026-08-27 | v11 | BR_4.6.2.13 — Dashboard vs. Discord avatar precedence closed | Closing blockquote deferred the question: the My POD data contract "including whether a Discord-sourced profile picture is ever used as a secondary fallback" was owned by UC_4.14.2, the client had not answered, and it "must be resolved when UC_4.14.2 is authored". | ✅ Closed by removing the contest. StackTrading never reads an avatar or a name from the Discord API — not as a primary value, not as a fallback, on any surface. A table splits the two worlds: Dashboard renders the uploaded photo or initials; Discord renders whatever the trader set there. Client rationale recorded: broken images and expired CDN links. Stated consequence for UC_4.14.2 — the My POD contract is now constrained, not open; the Discord-fallback branch is excluded and needs no design there. Extended to names as well. | BA/client answer 2026-08-27, Phần II — closes QnA A-20, and removes the corresponding item from UC_4.14.2's inbound question list. |
| 2026-08-27 | v11 | BR_4.6.2.12 — phone change: launch scope confirmed | Stated no rate limit, no OTP, no notification email, no propagation — sourced to the client's "STILL OPEN" QnA row, i.e. written as an assumption. | ✅ Confirmed for launch. No cool-down, and no security constraint of any kind: a phone change passes exactly two gates — the checkout regex and the system-wide uniqueness check — then writes straight to PostgreSQL. The rate-limit bullet now states plainly that this is a build decision taken in the absence of a client limit, not a limit the client ruled out, so a later client answer lands cleanly. | BA/client answers 2026-08-27, Phần II — closes QnA A-16 and A-17 for launch. The 2FA / Drawdown-Defense caveat already in the BR is unaffected and still applies if either capability is activated. |
| 2026-08-24 | v10 | Phone error copy. BR_4.6.2.3 rewritten. Also updated: §1 Description, §7 row 5, BR_4.6.2.4, BR_4.6.2.5, BR_4.6.2.8, §9.1 row 11. IN-08 and IN-09 retired and IN-06 widened in list-toast-popup.md v3.13. | The phone field reused the checkout rules (CR-09 §9.2 structure, E.164 6–15 digits, uniqueness) but deliberately not the checkout copy: BR_4.6.2.3 declared "the one deliberate deviation from CR-09 §9.2 is the error string" and Settings rendered its own IN-08 "Invalid phone number format. Please check and try again" and IN-09 "This phone number is already registered with another account". There was no empty-field error at all. | The deviation is removed — the field now conforms to CR-09 §9.2 and the checkout block completely, copy included. BR_4.6.2.3 now carries a 4-row error table: empty → "Please enter phone number" (CR-09 §9.2, new case — previously unspecified); invalid format/length → "Please enter a valid phone number" (CR-09 §9.2); duplicate → IN-06 "This phone number is already registered to another account." (the checkout string); same-as-current → IN-10, kept as the only Settings-specific string because neither CR-09 §9.2 nor the checkout field has a counterpart. IN-08 and IN-09 are retired system-wide. | BA instruction 2026-08-24: "chỉnh lại validation rule cho phone number, refer đến 9.2 Phone Number Field của Common Rule và checkout block, không sử dụng validation rule text hiện tại của field phone number hiện tại nữa. Ví dụ như 'Invalid phone number format. Please check and try again' sẽ không sử dụng nữa." This reverses the 2026-08-11 decision in QnA_init_docs.md A-09/A-10/A-12, which had rejected reusing CR-09/IN-06 and mandated Settings-only strings. CR-09 §9.2 is not edited — Settings conforms to it, so UC_2.8.3 (checkout) is behaviourally unchanged; only IN-06's trigger text broadened to name both screens. The two-independent-commit-paths model (BR_4.6.2.14, v7) was re-confirmed by the BA in the same instruction and is unchanged — photo commits on Crop Image [Done], phone commits on [Save Changes]. |
| 2026-08-21 | v9 | CF-03 copy and buttons; new §9.3 Screen Description table. Also updated: §6 Alternative Flow B steps 2–4, BR_4.6.2.14, BR_4.6.2.15, §9.1 row 7. CF-03 rewritten in list-toast-popup.md v3.11. | v7 introduced CF-03 as a BA-authored placeholder, flagged ❓ because no design frame existed: body copy "Remove your profile photo? Your avatar will revert to your initials across the Dashboard and the current image cannot be recovered." and buttons Cancel / Remove. The popup had no Screen Description table. | Replaced with the actual design: Title "Remove your photo?", no body text, buttons [Confirm] / [Cancel] stacked vertically with Confirm on top (Primary/gold). Every reference to the affirmative button renamed [Remove] → [Confirm]. Added §9.3 with the three-row field table and a note that this popup's button layout deliberately differs from CF-01/CF-02. ❓ flag removed. | BA supplied the design frame 2026-08-21, confirming the popup exists as derived in v7. Only the copy and the button treatment changed — the behaviour specified in v7 (immediate + irreversible commit, no participation in the dirty state, PATCH /profile-photo with profile_photo_url: null) is unchanged. |
| 2026-08-21 | v8 | API contract. New BR_4.6.2.15. Also updated: §Document References (new row 3b), §1 Description, §5 step 8, §6 Alt Flow A step 4.3, §6 Alt Flow B step 4.2, §7 rows 2/4/5/6, BR_4.6.2.2, BR_4.6.2.4, BR_4.6.2.6, BR_4.6.2.11, BR_4.6.2.13, BR_4.6.2.14, §9.1 rows 7/9/13, §9.2 row 3. OV-09 and CF-03 re-pointed in list-toast-popup.md v3.10. | One write endpoint, POST /profile, taking {user_id, profile_photo_url (Optional), phone_number (Optional)} — the shape given verbatim in RFQ_ Stack Trading Prop Tech V7.pdf §Page 36. Both commit paths introduced in v7 called that same endpoint with a different subset of fields. | Three changes, all from the Sotatek BE team. (1) POST → PATCH on every write — the operation is a partial update, never a creation. (2) Upload and submission are one operation. The RFQ's "S3 signed URL execution before submission" described a preparatory step ahead of a separate save; that save no longer exists, so signed URL → PUT S3 → PATCH now run as one uninterruptible sequence behind OV-09. The signed-URL mechanism is retained — the binary still never transits the application backend. (3) The write endpoint is split in two: PATCH /profile-photo {user_id, profile_photo_url} for the photo path, PATCH /profile {user_id, phone_number} for the phone path. GET /profile is unchanged. | Sotatek BE team API review, 2026-08-21, relayed by the BA: "POST /profiles → PATCH /profiles (chuẩn RESTful) · phải gộp bước image upload và bước submission làm 1 · suggest chuyển sang 2 api PATCH /profile-photo và PATCH /profiles riêng biệt". Two readings were clarified with the BA before writing: (a) Mechanism — "gộp làm 1" is about the trader-facing step count, not about collapsing the network hops; the signed-URL flow stays, so the RFQ's own upload mechanism is not contradicted, only its "before submission" framing. (b) Path naming — BE wrote /profiles (plural) but also /profile-photo (singular); the doc and RFQ use /profile. Normalised to singular across all three, since this is a singleton resource scoped to the authenticated trader. ❓ [PP-EP-01] (client confirmation of endpoint names) stays PENDING — this is a BE decision, not a client answer, and WBS/RFQ/this doc still disagree. |
| 2026-08-21 | v7 | Commit model of the whole screen. New BR_4.6.2.14. Rewritten: §1 Description, §4 Post-conditions, §5 steps 5–9, §6 Alternative Flow A, §6 Alternative Flow B, §7 rows 2–7, BR_4.6.2.6, BR_4.6.2.8, §9.1 rows 5/6/7/12/13, §9.2 rows 3/4. Two new IDs in list-toast-popup.md v3.9: OV-09, CF-03. | One form, one commit. Photo and phone were both staged locally: the Crop Image [Done] button only produced a preview and marked the form dirty, the S3 upload ran at §5 step 8 as part of [Save Changes], and a single POST /profile carried {profile_photo_url, phone_number} together. [Remove Photo] likewise only staged profile_photo_url = null, was reversible via [Cancel], and had no confirmation popup. The disposition of the old S3 object was declared out of scope. | Two independent commit paths. (1) Photo commits immediately. [Done] closes the modal and runs signed URL → PUT S3 → POST /profile {user_id, profile_photo_url} behind the new blocking overlay OV-09 "Updating avatar..."; on HTTP 200 the backend hard-deletes the previous S3 object. (2) Phone still commits on [Save Changes], now with a payload of {user_id, phone_number} only. (3) The photo no longer participates in the dirty state — a photo-only change leaves [Save Changes] Disabled and [Cancel] hidden, and [Cancel] can no longer revert a photo. (4) [Remove Photo] is now gated by the new confirmation popup CF-03 and commits immediately + irreversibly. §7 gained separate failure rows for the photo path and the phone path, both stating that a failure on one path never rolls back the other. | BA instruction 2026-08-21, with 3 reference frames (default screen with phone inline error · Crop Image modal · full-screen "Updating avatar..." overlay): "photo sẽ thay riêng, còn phone sẽ là thay riêng… khi trader click Done thì sẽ là upload và applied luôn ảnh mới, auto xoá ảnh cũ trong Database… photo không liên quan gì đến save changes cả. Nếu trader chỉ thay photo thôi không sửa phone number thì nút save changes không có sáng". Two points were derived rather than stated and confirmed with the BA in the same session: [Remove Photo] must also commit immediately (with [Save Changes] out of the picture there is no other path to persist a removal) and therefore needs a confirmation popup, since it is no longer undoable via [Cancel]; and the old S3 object is hard-deleted synchronously rather than via a cleanup job. ❓ Open with the client: the CF-03 copy and the "Updating avatar..." overlay copy are BA-authored — the design set has no frame for the removal confirmation. |
| 2026-08-12 | v6 | BR_4.6.2.1 — tab list, Billing row | Plain text UC_4.6.3 (not yet documented in this repo) | Real link to the now-published UC_4.6.3_v1.md, plus the two facts a reader of this tab list needs: the tab holds only the Invoice History table, and it is not LIVE-gated the way Market Data Management is | UC_4.6.3 SRS created (Init Flow — Agent 3, vòng 4). Every tab in the Account screen now points at a published UC. Link-only change — no behavioural change in this document. Ref: QnA_init_docs.md D-18. |
| 2026-08-12 | v5 | BR_4.6.2.11, §9.1 row 3, and the UC_4.6.x range note — Controls panel owner and label | UC_4.6.4 referenced as plain text and marked "not yet documented in this repo"; panel labelled "AI Chat and Reduce Motion" | Real link to the now-published UC_4.6.4_v1.md; panel labelled "Desk Manager and Reduce Motion" | Two changes, neither behavioural for this UC. (1) The Controls SRS now exists, so the placeholder note is replaced by a link. (2) Client-directed label change AI Chat → Desk Manager (Source: QnA from clients — Settings, Controls & Concierge UI/UX row, Adrian Stack 2026-08-10); the data contract (ai_enabled) is unchanged. Ref: QnA_init_docs.md C-01. |
| 2026-08-11 | v4 | BR_4.6.2.1 — tab list, Market Data Management and Account Actions rows, plus a new note on the UC_4.6.x range; BR_4.6.2.11 and §9.1 row 3 — Controls panel owner | Market Data Management = "No UC_ID assigned in the UC_4.6.x range; owning UC not yet identified in the WBS." · Account Actions = UC_4.6.4 — Resign Account (not yet documented in this repo) · Controls panel = UC_4.6.5 | Market Data Management = UC_4.17.1 (LIVE-only, not yet documented). Account Actions = not a single UC — Reset Associate Track → UC_4.11.1 (SIM, no LIVE counterpart), Resign & Close Account → UC_4.11.2 (SIM) / UC_4.17.2 (LIVE). Controls panel = UC_4.6.4. Added a note that the WBS common Settings range is only UC_4.6.1 – UC_4.6.4, that no UC_4.6.5 exists, and that the folder name is not an index. | ID-only remap against the current WBS (References/WBS/[BA Internal] Stacktrading.csv lines 81–84, 99–100, 117–118), approved by BA 2026-08-11. The WBS moved Account Actions out of the common range and re-used UC_4.6.4 for Controls, so the two stale mappings contradicted each other and each other's neighbours. No screen behaviour, field, or business rule changed in this document. Ref: UC_4.11.1 QnA_init_docs.md A-19. |
| 2026-08-11 | v3 | New BR_4.6.2.13 — Avatar Propagation. Also updated: §1 Overview, §4 Post-conditions, §5 step 10, BR_4.6.2.1, BR_4.6.2.7, §9.1 rows 7 and 13. | The avatar was described only as a field on this screen. The only stated consumer was "the sidebar user chip", with no detail on its collapsed vs. expanded states and no mention of the POD system at all. BR_4.6.2.7's closing line scoped the initials rule to this screen plus the chip. | Added BR_4.6.2.13 with the full consumer table (sidebar chip collapsed + expanded, My POD Pod Leader card, My POD members list on the LIVE Community page) plus the propagation mechanics: read-through rather than push, per-displayed-user fallback, refresh timing, and the fact that level/track/name labels are owned by each consumer surface rather than by this UC. BR_4.6.2.7 now scopes the initials rule to every avatar surface. The [Remove Photo] and [Save Changes] rows gained a matching Impact line. | BA instruction 2026-08-11 (Ref: QnA_init_docs.md A-20), with 3 reference frames: sidebar chip collapsed (SD / Lvl 5), sidebar chip expanded (photo · Sam Smith · Level 15), My POD widget (leader Marcus Chen photo + member rows showing MC initials). This closes the client question "Which screens will this avatar be used on?" left unanswered in QnA from clients — Settings, Profile & Preferences Definition row. The Dashboard-avatar vs. Discord-avatar precedence half of that question stays open and is deferred to UC_4.14.2. |
| 2026-08-11 | v2 | BR_4.6.2.1 — tab list, Connections & Credentials row | Plain text UC_4.6.1 (not yet documented in this repo) | Real link to the now-published UC_4.6.1_v1.md, plus the note that it is the default active tab on entry | UC_4.6.1 SRS created (Init Flow — Agent 3, vòng 2). ID/link-only change — no behavioural change in this document. |
| 2026-08-11 | v1 | Initial version | — | UC_4.6.2 created: §1–§9, 12 Business Rules (BR_4.6.2.1 → BR_4.6.2.12), 2 Screen Description tables (main tab + Crop Image modal). | Init Flow (Agent 1 Auditor → Agent 2 Challenger → Agent 3 Architect). BA answers recorded in QnA_init_docs.md A-01 → A-19. Four new IDs added to list-toast-popup.md v3.4 for this UC: TS-DASH-01, IN-08, IN-09, IN-10 — CR-09 §9.2 and IN-06 were deliberately left unchanged so UC_2.8.3 (checkout) is unaffected. |