[Issue]: Allow cancelling of POs and optional supersede by another PO #53

Closed
opened 2026-06-20 20:51:24 +00:00 by shad0w · 1 comment
Owner

Raised by

Kaushal Pal Singh (kps@pelagiamarine.com, MANAGER) — via portal Report Issue button

Description

When a PO gets cancelled it's financials should get subtracted from the trackers and the graphs. Allow POs to be cancelled by managers only. Ensure sufficient warning is given to the manager while cancelling (modal that has them type "cancel" to confirm).
Optionally allow cancelled POs to be superseded by other POs. The cancelled PO will link to the one it is superseded by

Priority

P2 — Medium

Context

  • Page: /po/cmqmltsf3006e8bro27eg5hww
  • Reported at: 2026-06-20T20:51:24.913Z
### Raised by Kaushal Pal Singh (kps@pelagiamarine.com, MANAGER) — via portal Report Issue button ### Description When a PO gets cancelled it's financials should get subtracted from the trackers and the graphs. Allow POs to be cancelled by managers only. Ensure sufficient warning is given to the manager while cancelling (modal that has them type "cancel" to confirm). Optionally allow cancelled POs to be superseded by other POs. The cancelled PO will link to the one it is superseded by ### Priority P2 — Medium ### Context - Page: `/po/cmqmltsf3006e8bro27eg5hww` - Reported at: 2026-06-20T20:51:24.913Z
shad0w added the
portal
label 2026-06-20 20:51:24 +00:00
shad0w added the
feature
interactive
triaged
labels 2026-06-20 21:02:19 +00:00
Author
Owner

Claude triage

Triage — Issue #53: Allow cancelling of POs and optional supersede by another PO

Type: Feature (new capability — adds a terminal cancelled state plus supersede linkage)
Routing: interactive
Priority: P2 — Medium

Interpretation

Two related requests:

  1. Cancel a PO (manager-only). A new terminal CANCELLED state. A cancelled PO's
    financials must be excluded from all spend trackers and graphs. The action is gated to
    MANAGER (and likely SUPERUSER). The UI must show a strong confirmation modal that forces the
    manager to type the word cancel before the action fires.
  2. Optional supersede (cancel + replace). A cancelled PO may be linked to the PO that
    replaces it (supersededById self-relation). This is described as optional/secondary.

Action items

A. Data model (Prisma migration — disqualifier on its own)

  • Add CANCELLED to the POStatus enum (prisma/schema.prisma:20).
  • Add a self-referential relation on PurchaseOrder for supersede:
    supersededById String? + supersededBy/supersededByOf relation pair, plus
    cancelledAt DateTime? and likely a cancellationReason String?.
  • Add CANCELLED (and optionally SUPERSEDED_LINKED) to the ActionType enum for the audit trail.
  • Generate + apply a migration. Runs against pelagia_test (prod mirror) per CLAUDE.md.

B. State machine (lib/po-state-machine.ts)

  • Add a cancel POAction and decide which source states it is valid from (e.g. any
    non-terminal pre-payment state? after approval? after payment? — see open questions). Restrict
    allowedRoles to ["MANAGER", "SUPERUSER"].
  • Decide whether cancel requires a note / side effects (email submitter + accounts likely).

C. Permissions (lib/permissions.ts) — disqualifier (auth/permissions)

  • Add a cancel_po permission granted to MANAGER (+ SUPERUSER), and enforce it in the new
    server action.

D. Server action + UI

  • New cancelPo (and supersede-link) server action under app/(portal)/po/[id]/actions.ts,
    calling requirePermission / canPerformAction, writing a POAction audit row, and
    revalidatePath for /po/[id], /dashboard, /history, /my-orders.
  • New client component: confirmation modal requiring the literal string cancel to enable the
    button. Wire a "Cancel PO" button into components/po/po-detail.tsx (manager-only, gated by
    current status).
  • Supersede UI: a way to pick/link the replacement PO and render the "superseded by →" link on
    both POs in po-detail.tsx.

E. Financial exclusion (money/aggregation — disqualifier) — must be exhaustive

A cancelled PO must drop out of every spend/count aggregation. Known sites to audit:

  • app/(portal)/dashboard/page.tsx — Manager "Total Approved Spend", "Approved This Month",
    vessel breakdown, 12-month series; all keyed off POST_APPROVAL_STATUSES
    (lib/utils.ts:83). Simplest path: keep CANCELLED out of that constant — but verify every
    consumer.
  • app/(portal)/admin/vessels/[id]/page.tsx:43, admin/sites/[id]/page.tsx,
    admin/vendors/[id]/page.tsx — per-entity spend totals filtering on
    CLOSED/PAID_DELIVERED.
  • app/api/reports/export/route.ts and app/api/po/[id]/export/route.ts — report/export
    status filters.
  • app/(portal)/history/history-filters.tsx, my-orders/page.tsx, payments/* — listing and
    counts.

F. Exhaustive enum maps (build will fail without these)

Record<POStatus, …> maps must get a CANCELLED entry or tsc breaks:

  • PO_STATUS_LABELS, PO_STATUS_VARIANTS in lib/utils.ts (add a danger/grey badge).
  • Confirm the export route's PO_STATUS_LABELS lookup is covered.

G. Side-effects to reverse on cancel

  • Inventory: approval increments ItemInventory
    (app/(portal)/approvals/[id]/actions.ts:61). Cancelling an already-approved PO with a
    siteId should reverse the increment — needs an explicit decision.
  • Vendor verification / payments: if a PO was already paid, what does cancel mean? Likely
    cancel should be blocked once paid (open question).

Files / areas involved

  • prisma/schema.prisma (enum + self-relation + fields → migration)
  • lib/po-state-machine.ts, lib/permissions.ts, lib/utils.ts
  • app/(portal)/po/[id]/actions.ts, components/po/po-detail.tsx, new modal component,
    components/po/po-status-badge.tsx
  • Dashboard, admin breakdown pages, reports/export routes, history, my-orders, payments
  • Possibly lib/notifier.ts (cancellation email event)

Open questions

  1. From which statuses is cancellation allowed? Pre-approval only, or also after approval /
    sent-for-payment? Is a paid/closed PO cancellable at all?
  2. Should cancellation require a reason/note (recommended for audit)?
  3. Supersede semantics: must the replacement PO already exist, or is it created from the cancel
    flow? Does linking enforce same vessel/account/vendor? Can the link be added later?
  4. Should the replacement PO show a reciprocal "supersedes PO X" link?
  5. Should cancelled POs still be exportable / appear in history (greyed) or be hidden?
  6. Confirm exact role set (MANAGER only, or MANAGER + SUPERUSER).
  7. Should approval-time inventory increments be reversed on cancel?

Routing rationale

Routing rationale: interactive — this is a large multi-file feature that requires a Prisma
migration (new enum value + self-referential supersede relation), permissions/auth changes
(manager-only cancel_po), and money/aggregation changes across many trackers, plus
underspecified supersede semantics and side-effect reversal — all explicit disqualifiers for an
unattended claude-queue run.

Routing: interactive | Type: feature

## Claude triage # Triage — Issue #53: Allow cancelling of POs and optional supersede by another PO **Type:** Feature (new capability — adds a terminal cancelled state plus supersede linkage) **Routing:** interactive **Priority:** P2 — Medium ## Interpretation Two related requests: 1. **Cancel a PO (manager-only).** A new terminal `CANCELLED` state. A cancelled PO's financials must be **excluded from all spend trackers and graphs**. The action is gated to MANAGER (and likely SUPERUSER). The UI must show a strong confirmation modal that forces the manager to type the word `cancel` before the action fires. 2. **Optional supersede (cancel + replace).** A cancelled PO may be linked to the PO that replaces it (`supersededById` self-relation). This is described as optional/secondary. ## Action items ### A. Data model (Prisma migration — disqualifier on its own) - Add `CANCELLED` to the `POStatus` enum (`prisma/schema.prisma:20`). - Add a self-referential relation on `PurchaseOrder` for supersede: `supersededById String?` + `supersededBy`/`supersededByOf` relation pair, plus `cancelledAt DateTime?` and likely a `cancellationReason String?`. - Add `CANCELLED` (and optionally `SUPERSEDED_LINKED`) to the `ActionType` enum for the audit trail. - Generate + apply a migration. **Runs against `pelagia_test` (prod mirror)** per CLAUDE.md. ### B. State machine (`lib/po-state-machine.ts`) - Add a `cancel` `POAction` and decide **which source states** it is valid from (e.g. any non-terminal pre-payment state? after approval? after payment? — see open questions). Restrict `allowedRoles` to `["MANAGER", "SUPERUSER"]`. - Decide whether cancel requires a note / side effects (email submitter + accounts likely). ### C. Permissions (`lib/permissions.ts`) — disqualifier (auth/permissions) - Add a `cancel_po` permission granted to MANAGER (+ SUPERUSER), and enforce it in the new server action. ### D. Server action + UI - New `cancelPo` (and supersede-link) server action under `app/(portal)/po/[id]/actions.ts`, calling `requirePermission` / `canPerformAction`, writing a `POAction` audit row, and `revalidatePath` for `/po/[id]`, `/dashboard`, `/history`, `/my-orders`. - New client component: confirmation modal requiring the literal string `cancel` to enable the button. Wire a "Cancel PO" button into `components/po/po-detail.tsx` (manager-only, gated by current status). - Supersede UI: a way to pick/link the replacement PO and render the "superseded by →" link on both POs in `po-detail.tsx`. ### E. Financial exclusion (money/aggregation — disqualifier) — must be exhaustive A cancelled PO must drop out of every spend/count aggregation. Known sites to audit: - `app/(portal)/dashboard/page.tsx` — Manager "Total Approved Spend", "Approved This Month", vessel breakdown, 12-month series; all keyed off `POST_APPROVAL_STATUSES` (`lib/utils.ts:83`). Simplest path: keep `CANCELLED` out of that constant — but verify every consumer. - `app/(portal)/admin/vessels/[id]/page.tsx:43`, `admin/sites/[id]/page.tsx`, `admin/vendors/[id]/page.tsx` — per-entity spend totals filtering on `CLOSED`/`PAID_DELIVERED`. - `app/api/reports/export/route.ts` and `app/api/po/[id]/export/route.ts` — report/export status filters. - `app/(portal)/history/history-filters.tsx`, `my-orders/page.tsx`, `payments/*` — listing and counts. ### F. Exhaustive enum maps (build will fail without these) `Record<POStatus, …>` maps must get a `CANCELLED` entry or `tsc` breaks: - `PO_STATUS_LABELS`, `PO_STATUS_VARIANTS` in `lib/utils.ts` (add a `danger`/grey badge). - Confirm the export route's `PO_STATUS_LABELS` lookup is covered. ### G. Side-effects to reverse on cancel - **Inventory:** approval increments `ItemInventory` (`app/(portal)/approvals/[id]/actions.ts:61`). Cancelling an already-approved PO with a `siteId` should reverse the increment — needs an explicit decision. - **Vendor verification / payments:** if a PO was already paid, what does cancel mean? Likely cancel should be blocked once paid (open question). ## Files / areas involved - `prisma/schema.prisma` (enum + self-relation + fields → migration) - `lib/po-state-machine.ts`, `lib/permissions.ts`, `lib/utils.ts` - `app/(portal)/po/[id]/actions.ts`, `components/po/po-detail.tsx`, new modal component, `components/po/po-status-badge.tsx` - Dashboard, admin breakdown pages, reports/export routes, history, my-orders, payments - Possibly `lib/notifier.ts` (cancellation email event) ## Open questions 1. From which statuses is cancellation allowed? Pre-approval only, or also after approval / sent-for-payment? Is a paid/closed PO cancellable at all? 2. Should cancellation require a reason/note (recommended for audit)? 3. Supersede semantics: must the replacement PO already exist, or is it created from the cancel flow? Does linking enforce same vessel/account/vendor? Can the link be added later? 4. Should the replacement PO show a reciprocal "supersedes PO X" link? 5. Should cancelled POs still be exportable / appear in history (greyed) or be hidden? 6. Confirm exact role set (MANAGER only, or MANAGER + SUPERUSER). 7. Should approval-time inventory increments be reversed on cancel? ## Routing rationale Routing rationale: interactive — this is a large multi-file feature that requires a Prisma migration (new enum value + self-referential supersede relation), permissions/auth changes (manager-only `cancel_po`), and money/aggregation changes across many trackers, plus underspecified supersede semantics and side-effect reversal — all explicit disqualifiers for an unattended claude-queue run. **Routing:** `interactive` | **Type:** `feature`
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: shad0w/pelagia-portal#53
No description provided.