At a Glance
Persona: Vendor (external party — token-authenticated portal only) · Module: vendor-pricelist · Scenarios: ~25
Categories: Happy Path · Permission (N/A) · Validation · Edge Case
E2E coverage: none — vendor portal is a separate public surface; coverage at API / integration level (token middleware, validator output, auto-save) pending roadmap item in../carmen-inventory-frontend-e2e/
This page captures the test scenarios that exercise the Vendor persona's interaction with the vendor-pricelist module. Unlike most external personas in the Carmen suite, the vendor here drives system state directly through a token-authenticated portal session — the only place an external party operates inside the Carmen ecosystem with persistent in-system effect (03-user-flow-vendor.md Section 1). The vendor's surface is bounded: open the portal via per-vendor URL, pick a submission currency, enter or upload pricing for the template's product list, save drafts (auto-save every ~30 s), and click Submit. Approval, rejection, inactivation, expiry, and token-policy controls are all driven by internal personas (Purchaser / Manager / Finance Manager / Sysadmin / cron). Because the vendor has no Carmen login and no Carmen-side RBAC matrix, the Permission / Authorization section below is intentionally reduced to a single N/A row — the portal-token policy itself is tested as a Sysadmin-side configuration concern in 04-test-scenarios-audit-config.md, and segregation of duties (VPL_AUTH_014) is verified from the Purchaser / Auditor side. Cross-persona handoffs that pivot off the vendor's submission (X-VPL-01, X-VPL-04, X-VPL-05, X-VPL-07, X-VPL-08) live in the parent overview, not here.
| # | Scenario | Pre-condition | Steps | Expected |
|---|---|---|---|---|
| VPL-VND-HP-01 | Open the portal via invitation link (first access) | Purchaser launched campaign per VPL-PUR-HP-02; vendor V1 received invitation email at contact_email; portal URL embeds tb_request_for_pricing_detail.pricelist_url_token for V1 |
1. Vendor clicks the URL. 2. Portal exchanges token for session cookie per VPL_AUTH_007. 3. Welcome page renders with invitation details (campaign name, deadline countdown via VPL_CALC_007, custom message), the three submission options (Direct online entry / Excel upload / Email submission), and the validation panel placeholder. |
First-access system comment is appended in tb_request_for_pricing_detail_comment capturing the access timestamp and (where configured) the source IP. Derived invitation status moves pending → in-progress per VPL_POST_011. No tb_pricelist row is created yet — it materialises on first save. |
| VPL-VND-HP-02 | Select submission currency on first save | Vendor at portal welcome page; tenant permitted-currency list = [THB, USD, EUR]; vendor wants to quote in EUR |
1. Pick Direct online entry. 2. Currency picker → select EUR. 3. Enter price for first product. 4. Auto-save fires within 30 s. |
tb_pricelist row inserted at status = draft with currency_id = EUR, currency_code = EUR, submission_method = online, submitted_at = NULL; FK tb_request_for_pricing_detail.pricelist_id populated. VPL_AUTH_008 validation passes (EUR is in the tenant-permitted list). Auto-save system comment captures the change. |
| VPL-VND-HP-03 | Enter multi-MOQ-tier pricing for one product | Vendor on the online-entry surface; template product P1 with default order unit Each and template MOQ tiers [{qty:1}, {qty:50}, {qty:100}] |
1. Tier 1 row: price_without_tax = ฿12.50, tax_rate = 0.07. 2. Tier 2 row (via [+] button): qty 50, price_without_tax = ฿10.50. 3. Tier 3: qty 100, price_without_tax = ฿9.75. 4. Auto-save. |
Three tb_pricelist_detail rows persisted under @@unique([pricelist_id, product_id, unit_id, moq_qty, deleted_at]). Tax decomposition computed per VPL_CALC_001–VPL_CALC_002. Validator runs VPL_VAL_020 and passes (non-increasing prices: 13.375 > 11.235 > 10.4325). Inline real-time feedback: green check on each row. |
| VPL-VND-HP-04 | Upload Excel template (portal upload) | Vendor downloaded the Excel template earlier, filled offline, returns to portal | 1. From welcome page, pick Download Excel template and upload. 2. Drag-and-drop the completed workbook onto the upload zone. 3. Application parses each row into tb_pricelist_detail rows. 4. Review parsed result. 5. Save. |
Pricelist rows materialised with submission_method = portal; parser-side validation runs against VPL_VAL_018–VPL_VAL_022 and reports any row-level errors inline (e.g., "Row 7: MOQ qty must be non-negative"). Errors are corrected inline before Submit. |
| VPL-VND-HP-05 | Auto-save and resume across sessions | Vendor partway through entering pricing; closes browser at 45% complete | 1. Vendor enters first 5 rows, then closes the browser. 2. Two hours later, vendor returns to the invitation email and clicks the same URL. 3. Portal re-authenticates the token (still within validity window, no session-limit hit, token not revoked). 4. Welcome page renders with Resume draft action. 5. Vendor clicks Resume; the 5 previously-entered rows are loaded. | Token still valid per VPL_AUTH_007; session resumed; tb_pricelist row unchanged; the 5 saved rows render in their last-saved state; the validator's running output reflects the partial completion. No data loss. |
| VPL-VND-HP-06 | Submit pricelist after completion | Vendor has entered pricing for all template products; validator reports quality_score = 88 and zero blocking errors |
1. Vendor clicks Submit. 2. Validator runs full pass per VPL_VAL_023. 3. On success, confirmation page renders with submission reference. |
VPL_POST_016 fires: tb_pricelist.submitted_at = now(), submission_method finalised, quality_score written to info; system comment in tb_pricelist_comment records submission; confirmation email dispatched to vendor's contact_email; invitation status moves to derived submitted per VPL_POST_012. Purchaser is notified. |
| VPL-VND-HP-07 | Recall a submitted pricelist before Purchaser approval | Vendor submitted in VPL-VND-HP-06; Purchaser has not yet approved (status still draft + submitted_at IS NOT NULL) |
1. Vendor re-opens the portal via the same URL. 2. Sees the submitted pricelist with Recall and edit action available. 3. Clicks Recall. | submitted_at reset to NULL; pricelist returns to editable draft surface; recall system comment appended; Purchaser is notified that the submission was withdrawn. Vendor can now edit and resubmit. |
| VPL-VND-HP-08 | Resubmit after Purchaser rejection | Vendor's prior submission was rejected by Purchaser per VPL-PUR-HP-05 with reason; original token still valid | 1. Vendor receives rejection email + reason. 2. Opens portal via the original URL. 3. Sees the rejection system comment on the pricelist. 4. Corrects the flagged row (e.g., updates the zero-price row with a valid price + FOC explanation or removes it). 5. Clicks Submit. |
submitted_at = now() set again; new system comment captures the resubmission; the Purchaser's review queue receives the resubmitted pricelist; the rejection comment is preserved on the activity log so the audit chain shows the cycle. |
| VPL-VND-HP-09 | Use email submission method (vendor cannot use portal) | Vendor confirmed by phone they cannot use the portal but have the Excel template (downloaded out-of-band or by Purchaser sending it manually) | 1. Vendor fills the Excel offline. 2. Sends to purchasing's email contact. 3. Purchaser uploads on vendor's behalf per VPL-PUR-HP-06 (VPL_AUTH_003). |
No vendor portal session is opened; tb_pricelist.submission_method = email; system comment captures the email source. Vendor never touches Carmen UI; the staff-side upload is the equivalent of a manual VPL-VND-HP-06 on the vendor's behalf. |
| # | Scenario | Expected behaviour |
|---|---|---|
| VPL-VND-PERM-01 | Vendor is an external party with no Carmen system login; in-system RBAC is N/A. The portal-token policy (token expiration, IP allowlist, concurrent-session limits, suspicious-activity detection) is the only access-control surface for the vendor, and it is tested as a Sysadmin-side configuration concern. | N/A — there is no Carmen login for the vendor and no in-system RBAC matrix to enumerate. The vendor's "permissions" reduce to token validity per VPL_AUTH_007 and segregation of duties per VPL_AUTH_014 (the user holding the token cannot also be a Carmen user approving the pricelist) — both verified from the Sysadmin / Purchaser / Auditor side. Portal-token policy unit tests (expiration, IP allowlist, session limits, revocation propagation) are catalogued in 04-test-scenarios-audit-config.md. |
| # | Scenario | Trigger | Expected error |
|---|---|---|---|
| VPL-VND-VAL-01 | Portal access after token expiration | Campaign end_date < now(); cron VPL_POST_014 revoked the token; vendor clicks the URL the next day |
Reject — VPL_AUTH_007. Portal returns 401 — token expired or revoked with a help message directing the vendor to contact the purchasing team via the contact information on the original email. No pricelist row is created; no portal session is opened. |
| VPL-VND-VAL-02 | Portal access from a non-allowlist IP | Sysadmin configured IP allowlist for the tenant (e.g., specific vendor's office network); vendor tries to access from a personal mobile network outside the allowlist | Reject — VPL_AUTH_007. Portal returns 401 — access from this IP is not authorised for this token with guidance to use the approved network. Per-attempt system comment is written in tb_request_for_pricing_detail_comment capturing the non-allowlist IP for the Auditor's record (helping the Sysadmin tune the allowlist if legitimate traffic is being blocked). |
| VPL-VND-VAL-03 | Concurrent session limit exceeded | Default tenant limit = 5 concurrent sessions per token; vendor's procurement team opens the portal in 6 browser tabs simultaneously | Reject the 6th — VPL_AUTH_007. Portal returns 429 — concurrent session limit exceeded to the 6th tab; the first 5 remain active. The vendor can close one of the active tabs to free a slot, then retry the 6th. Tenant-configurable limit; lower limits route to escalation. |
| VPL-VND-VAL-04 | Submit a pricelist with zero detail rows | Vendor opened the portal, saved a draft header (tb_pricelist row exists at status = draft), but added no detail rows; clicks Submit |
Reject — VPL_VAL_023. Server returns "Pricelist must contain at least one valid line item to submit." Submit button is disabled in the UI until at least one row passes line-level validation; direct API call (an unusual path for a vendor) is rejected. |
| VPL-VND-VAL-05 | Submit MOQ-tier pricing in ascending order (validator catches inversion at submit) | Vendor enters MOQ 1 @ ฿10.00, then MOQ 50 @ ฿11.00, then MOQ 100 @ ฿12.00 (ascending, not the intended bulk-discount direction) |
Save emits warning per VPL_VAL_020; submit rejects. Server returns "MOQ-tier pricing must be non-increasing as MOQ quantity increases." Inline correction guidance highlights the offending tiers; the vendor must adjust before Submit can fire. |
| VPL-VND-VAL-06 | Submit a row with price_without_tax < 0 |
Vendor enters price_without_tax = -10.00 (perhaps via Excel upload from a malformed workbook) |
Reject — VPL_VAL_021. Server returns "Price-without-tax must be non-negative; tax_amt and price must reconcile to the configured rounding rule (5 dp internal, 2 dp display)." Inline error on the row; Submit blocked until corrected. |
| VPL-VND-VAL-07 | Select a non-permitted currency | Vendor picks BTC (or any other unpermitted currency) from a hypothetical extended picker |
Reject — VPL_AUTH_008. UI does not list non-permitted currencies in the picker; a direct API call with an unpermitted currency_id is rejected. Server returns "Currency is not permitted for this tenant." |
| VPL-VND-VAL-08 | Add the same product twice in online-entry mode (force duplicate via direct DOM manipulation) | Vendor's browser is compromised or vendor attempts a direct API call with two tb_pricelist_detail rows for the same (pricelist_id, product_id, unit_id, moq_qty) |
Reject — VPL_VAL_019 + the DB unique index pricelist_detail_pricelist_id_product_id_unit_id_moqqty_u. Server returns "MOQ quantity is required and must be non-negative; duplicate (product, unit, MOQ) rows are not permitted." UI prevents the duplicate; DB is the fallback guard. |
| # | Scenario | Condition | Expected |
|---|---|---|---|
| VPL-VND-EDGE-01 | Auto-save during portal-session network drop | Vendor on online-entry surface; network connection drops mid-typing; vendor types for 20 s with no network | Auto-save buffers locally in the browser and retries on next network connectivity. On success, the auto-save catches up with the last-typed value; a "saved" indicator appears in the UI. If the network is down at the 30-second auto-save mark, the previous saved value is retained on the server; the local buffer is preserved until the browser closes or the page is navigated away. No data loss in the typical case; documented behaviour for the worst case (browser crash during disconnection: up to the last 30 s of typing may be lost). |
| VPL-VND-EDGE-02 | Switch submission method mid-flight | Vendor enters 5 rows online, then decides to switch to Excel upload | Portal offers Switch to Excel upload with a confirmation prompt. On confirm, the 5 online-entered rows are discarded (a system comment captures the discard); upload surface renders. On Excel upload, new rows are parsed in. submission_method is finalised at the moment of Submit click — so a vendor can switch back and forth before Submit without locking in either method. After Submit, submission_method is immutable. |
| VPL-VND-EDGE-03 | Two vendor users from the same vendor company open the portal on different machines | Vendor V1 has procurement staff A and B; both receive the invitation forwarded internally; both click the URL simultaneously from different machines |
Both sessions are valid (within concurrent-session limit). They share the same tb_pricelist row (one pricelist per invitation). Edits from A and B are subject to the optimistic-concurrency guard on doc_version — the first save wins; the second save sees a stale version and is prompted to refresh. Real-world behaviour: this rarely happens because the two users coordinate offline, but the mechanic is preserved. |
| VPL-VND-EDGE-04 | Vendor portal session is open when the Sysadmin revokes the token | Vendor has an in-progress draft at the portal; Sysadmin revokes the token via VPL_AUTH_015 due to compromise |
Vendor's current page continues until the next API call (next auto-save, navigation, or Submit). The next API call returns 401 — token revoked; the portal redirects to a "session terminated" page with guidance to contact the purchasing team. Auto-save drafts written before revocation are preserved at tb_pricelist for the Purchaser's decision per VPL-PUR-EDGE-05. |
| VPL-VND-EDGE-05 | Auto-save fires exactly at campaign end_date boundary |
Vendor saves at T = end_date - 1ms; cron VPL_POST_014 fires at T = end_date and tries to expire the invitation |
Race resolved by atomic timestamp comparison. If the save's server-write timestamp is strictly less than end_date, the save succeeds and tb_pricelist.updated_at reflects the save; the cron's subsequent expire still fires but the saved draft is preserved. If the save's server-write timestamp is >= end_date, the save is rejected with "Campaign has closed — submissions are no longer accepted." The invitation moves to expired regardless; the draft is preserved for the Purchaser's audit. |
| VPL-VND-EDGE-06 | Submission with edge-case decimal — maximum MOQ qty | Vendor submits moq_qty = 99999999999.99999 (upper bound of Decimal(20, 5)) at price_without_tax = ฿0.10 |
Accepted at storage; tax_amt = Round(0.10 × 0.07, 5); price computed correctly per VPL_CALC_001–VPL_CALC_002. Effective unit price per base UoM computed correctly. The MOQ value renders correctly in display (formatted per locale). |
| VPL-VND-EDGE-07 | Vendor uploads an Excel with a product not on the template | Vendor's workbook includes a row for product P99 which is not in the template's product list |
Reject the row per VPL_VAL_018. Parser surfaces the error inline: "Row 12: Product P99 is not on the issuing template — please remove or contact purchasing for clarification." The remainder of the workbook is parsed and saved; the offending row is excluded with the error logged. The vendor either removes the row or contacts purchasing to add the product to the template (a Purchaser action under VPL_AUTH_001). |
X-VPL-01 (full happy path), X-VPL-04 (reject + resubmit), X-VPL-05 (email submission), X-VPL-07 (token revoked), X-VPL-08 (auto-expire).VPL_VAL_018–VPL_VAL_023 — line-level validations the vendor triggers on save and submit; VPL_VAL_020 MOQ-tier non-increasing check; VPL_VAL_023 minimum-one-row submit check), § 3 (VPL_CALC_001–VPL_CALC_003 line-price decomposition; VPL_CALC_007 deadline countdown), § 4 (VPL_AUTH_007 portal access via token; VPL_AUTH_008 currency / unit selection; VPL_AUTH_014 segregation of duties — verified from the Sysadmin / Purchaser side, not the Vendor side), § 5 (VPL_POST_010–VPL_POST_014 invitation lifecycle; VPL_POST_015–VPL_POST_016 vendor-driven pricelist transitions).tb_request_for_pricing_detail.pricelist_url_token (per-invitation cryptographic token); tb_pricelist.url_token (denormalised copy); the multi-MOQ-per-product unique key (pricelist_detail_pricelist_id_product_id_unit_id_moqqty_u) referenced in VPL-VND-VAL-08.app/(main)/vendor-portal/[token]/ with its own token-authenticated harness layer. Coverage today is at the API / integration level (token middleware, validation-engine output, auto-save buffering) tracked in ../carmen/docs/vendor-pricelist-management/tasks.md. Sibling reference: the external Vendor on purchase-order has no Carmen surface at all and is documented in purchase-order/04-test-scenarios-vendor as N/A across the board; this module's Vendor differs because the portal session is an in-system surface (subject to token policy) even though it is not under Carmen's main RBAC matrix.