mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 07:22:53 +00:00
feat: streamline clearance action handling and enhance document upload process
This commit is contained in:
@@ -15,10 +15,9 @@ export interface ActionItem {
|
||||
urgent?: boolean;
|
||||
}
|
||||
|
||||
const CLEARANCE_UPLOAD_STATUSES = [
|
||||
"AWAITING_CLEARANCE_DOCUMENTS",
|
||||
"CLEARANCE_UNDER_REVIEW",
|
||||
];
|
||||
// Only AWAITING is a pending CUSTOMER action (initial upload or re-upload after a
|
||||
// query). UNDER_REVIEW is waiting on staff, so it doesn't belong on the card.
|
||||
const CLEARANCE_ACTION_STATUSES = ["AWAITING_CLEARANCE_DOCUMENTS"];
|
||||
|
||||
/**
|
||||
* Derive the list of pending customer actions from the customer's contracts and
|
||||
@@ -43,17 +42,14 @@ export function deriveActionItems(
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (CLEARANCE_UPLOAD_STATUSES.includes(c.status)) {
|
||||
const queried = c.status === "AWAITING_CLEARANCE_DOCUMENTS";
|
||||
if (CLEARANCE_ACTION_STATUSES.includes(c.status)) {
|
||||
items.push({
|
||||
id: `clearance-${c.id}`,
|
||||
kind: "clearance",
|
||||
reference: c.reference,
|
||||
description: queried
|
||||
? "Clearance document needs correction"
|
||||
: "Upload clearance documents",
|
||||
description: "Clearance documents needed",
|
||||
targetId: c.id,
|
||||
urgent: queried,
|
||||
urgent: true,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -145,18 +145,26 @@ export function ActionNeededSection({ items }: ActionNeededSectionProps) {
|
||||
</Group>
|
||||
<Button
|
||||
size="compact-sm"
|
||||
variant="light"
|
||||
variant={item.urgent ? "filled" : "light"}
|
||||
color={meta.color}
|
||||
radius="md"
|
||||
leftSection={<FilePlus2 size={14} />}
|
||||
leftSection={
|
||||
item.kind === "clearance" ? (
|
||||
<Upload size={14} />
|
||||
) : (
|
||||
<FilePlus2 size={14} />
|
||||
)
|
||||
}
|
||||
>
|
||||
{item.kind === "pay"
|
||||
? "Pay"
|
||||
? "Pay now"
|
||||
: item.kind === "sign"
|
||||
? "Sign"
|
||||
: item.kind === "book"
|
||||
? "Book"
|
||||
: "Resolve"}
|
||||
: item.urgent
|
||||
? "Upload documents"
|
||||
: "Upload"}
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
|
||||
@@ -111,10 +111,21 @@ export function ContractClearancePanel({
|
||||
},
|
||||
});
|
||||
|
||||
const customerDocs = useMemo(
|
||||
() =>
|
||||
(clearance?.documents ?? []).filter((d) => d.uploadedBy === "customer"),
|
||||
[clearance],
|
||||
const customerDocs = useMemo(() => {
|
||||
const docs = (clearance?.documents ?? []).filter(
|
||||
(d) => d.uploadedBy === "customer",
|
||||
);
|
||||
// Surface queried documents (the ones needing correction) first.
|
||||
const rank = (s: string | null) =>
|
||||
s === "QUERIED" ? 0 : s === "APPROVED" ? 2 : 1;
|
||||
return [...docs].sort(
|
||||
(a, b) => rank(a.reviewStatus) - rank(b.reviewStatus),
|
||||
);
|
||||
}, [clearance]);
|
||||
|
||||
const queriedCount = useMemo(
|
||||
() => customerDocs.filter((d) => d.reviewStatus === "QUERIED").length,
|
||||
[customerDocs],
|
||||
);
|
||||
const glDocs = useMemo(
|
||||
() => (clearance?.documents ?? []).filter((d) => d.uploadedBy !== "customer"),
|
||||
@@ -167,6 +178,18 @@ export function ContractClearancePanel({
|
||||
|
||||
const body = (
|
||||
<Stack gap={0}>
|
||||
{queriedCount > 0 && (
|
||||
<Alert
|
||||
color="red"
|
||||
radius="md"
|
||||
icon={<AlertCircle size={18} />}
|
||||
mb="md"
|
||||
title={`${queriedCount} document${queriedCount > 1 ? "s" : ""} need correction`}
|
||||
>
|
||||
Re-upload the highlighted document{queriedCount > 1 ? "s" : ""} below to
|
||||
continue. The reviewer's note explains what to fix.
|
||||
</Alert>
|
||||
)}
|
||||
{isReady ? (
|
||||
<Alert color="teal" radius="md" icon={<CheckCircle2 size={18} />} mb="md">
|
||||
{customsPath
|
||||
@@ -201,7 +224,14 @@ export function ContractClearancePanel({
|
||||
<Box
|
||||
key={doc.fileKey}
|
||||
className="rounded-xl"
|
||||
style={{ border: `1px solid ${BORDER}`, padding: 12 }}
|
||||
style={{
|
||||
border:
|
||||
doc.reviewStatus === "QUERIED"
|
||||
? "1px solid #F0B4B4"
|
||||
: `1px solid ${BORDER}`,
|
||||
background: doc.reviewStatus === "QUERIED" ? "#FDF4F4" : "#fff",
|
||||
padding: 12,
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between" align="center" wrap="nowrap">
|
||||
<Group gap={10} wrap="nowrap" style={{ minWidth: 0 }}>
|
||||
|
||||
@@ -100,7 +100,9 @@ function groupContractDocuments(files: ContractFile[]): DocGroup[] {
|
||||
const profile: ContractFile[] = [];
|
||||
const clearance: ContractFile[] = [];
|
||||
for (const f of files) {
|
||||
// Signature images are baked into the contract PDF — don't list them here.
|
||||
if (f.code === "contract") contract.push(f);
|
||||
else if (f.code.startsWith("signature_")) continue;
|
||||
else if (PROFILE_DOC_CODES.has(f.code)) profile.push(f);
|
||||
else clearance.push(f);
|
||||
}
|
||||
@@ -444,6 +446,20 @@ export default function ContractDetailPage() {
|
||||
? "The Operations team is reviewing your clearance documents. Re-upload any queried documents to proceed."
|
||||
: "Your clearance documents are approved. You can now create a shipment booking under this contract."}
|
||||
</Text>
|
||||
{contract.status !== "CLEARANCE_READY_FOR_BOOKING" && (
|
||||
<Button
|
||||
mt="md"
|
||||
color="edr-green"
|
||||
radius="md"
|
||||
size="sm"
|
||||
leftSection={<Upload size={15} />}
|
||||
onClick={clearanceModal.open}
|
||||
>
|
||||
{contract.status === "AWAITING_CLEARANCE_DOCUMENTS"
|
||||
? "Upload documents"
|
||||
: "Manage documents"}
|
||||
</Button>
|
||||
)}
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
@@ -664,33 +680,75 @@ export default function ContractDetailPage() {
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
{files.length === 0 ? (
|
||||
<Stack align="center" gap={10} py="xl">
|
||||
<FileText size={26} color={MUTED} style={{ opacity: 0.5 }} />
|
||||
{docGroups.length === 0 ? (
|
||||
<Stack align="center" gap={10} py={48}>
|
||||
<Box
|
||||
style={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 16,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "#F1F5F9",
|
||||
}}
|
||||
>
|
||||
<FileText size={26} color={MUTED} style={{ opacity: 0.6 }} />
|
||||
</Box>
|
||||
<Text fz={14} fw={600} style={{ color: INK }}>
|
||||
No documents yet
|
||||
</Text>
|
||||
<Text fz={13} c="dimmed" ta="center" maw={420}>
|
||||
No documents yet. The signed contract and any uploaded
|
||||
clearance documents will appear here.
|
||||
The signed contract and any uploaded clearance documents will
|
||||
appear here.
|
||||
</Text>
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack gap="lg">
|
||||
{docGroups.map((group) => (
|
||||
<Stack key={group.key} gap={8}>
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fz={12} fw={700} c="dimmed" tt="uppercase">
|
||||
{group.title}
|
||||
</Text>
|
||||
<Badge size="xs" variant="light" color="gray" radius="sm">
|
||||
{group.files.length}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Stack gap={10}>
|
||||
{group.files.map((file) => (
|
||||
<DocFileRow key={file.id} file={file} onView={view} />
|
||||
))}
|
||||
<Stack gap="xl">
|
||||
{docGroups.map((group) => {
|
||||
const accent = DOC_GROUP_ACCENT[group.key] ?? GREEN;
|
||||
const Icon = DOC_GROUP_ICON[group.key] ?? FileText;
|
||||
return (
|
||||
<Stack key={group.key} gap={10}>
|
||||
<Group gap={10} align="center">
|
||||
<Box
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 8,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: `${accent}14`,
|
||||
color: accent,
|
||||
}}
|
||||
>
|
||||
<Icon size={15} />
|
||||
</Box>
|
||||
<Text fz={13} fw={700} style={{ color: INK }}>
|
||||
{group.title}
|
||||
</Text>
|
||||
<Badge
|
||||
size="sm"
|
||||
variant="light"
|
||||
color="gray"
|
||||
radius="sm"
|
||||
>
|
||||
{group.files.length}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Stack gap={10}>
|
||||
{group.files.map((file) => (
|
||||
<DocFileRow
|
||||
key={file.id}
|
||||
file={file}
|
||||
onView={view}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
)}
|
||||
</Card>
|
||||
@@ -813,6 +871,18 @@ const KEY_FACT_ACCENT: Record<string, string> = {
|
||||
orange: "#C77F09",
|
||||
};
|
||||
|
||||
// Per-section accent + icon for the Documents tab groups.
|
||||
const DOC_GROUP_ACCENT: Record<string, string> = {
|
||||
contract: GREEN,
|
||||
profile: "#2B6CB0",
|
||||
clearance: "#C77F09",
|
||||
};
|
||||
const DOC_GROUP_ICON: Record<string, LucideIcon> = {
|
||||
contract: FileSignature,
|
||||
profile: FileText,
|
||||
clearance: Upload,
|
||||
};
|
||||
|
||||
/**
|
||||
* A pill-style detail tab matching the backoffice booking-requests tabs: an
|
||||
* icon, a label, and an always-visible count badge (shows 0 when empty).
|
||||
@@ -897,6 +967,30 @@ function SectionLabel({
|
||||
* stored filename and size as secondary text, and view / download actions that
|
||||
* stream through the API by file id.
|
||||
*/
|
||||
/** Extension → a small colored type chip (PDF red, image green, etc.). */
|
||||
function fileTypeChip(name: string, mimeType?: string | null): {
|
||||
ext: string;
|
||||
color: string;
|
||||
} {
|
||||
const dot = name.lastIndexOf(".");
|
||||
let ext = dot >= 0 ? name.slice(dot + 1).toUpperCase() : "";
|
||||
if (!ext && mimeType) ext = mimeType.split("/")[1]?.toUpperCase() ?? "FILE";
|
||||
if (!ext) ext = "FILE";
|
||||
const color =
|
||||
ext === "PDF"
|
||||
? "#D64545"
|
||||
: ["PNG", "JPG", "JPEG", "GIF", "WEBP", "SVG"].includes(ext)
|
||||
? "#2F9E6E"
|
||||
: ["DOC", "DOCX"].includes(ext)
|
||||
? "#2B6CB0"
|
||||
: ["XLS", "XLSX", "CSV"].includes(ext)
|
||||
? "#2F855A"
|
||||
: ["MP4", "WEBM", "MOV"].includes(ext)
|
||||
? "#7A40C8"
|
||||
: "#6B7C8E";
|
||||
return { ext: ext.slice(0, 4), color };
|
||||
}
|
||||
|
||||
function DocFileRow({
|
||||
file,
|
||||
onView,
|
||||
@@ -905,15 +999,45 @@ function DocFileRow({
|
||||
onView: (f: ViewableFile) => void;
|
||||
}) {
|
||||
const kind = labelForDocCode(file.code);
|
||||
const { ext, color } = fileTypeChip(file.name, file.mimeType);
|
||||
const viewable = isViewable({
|
||||
name: file.name,
|
||||
url: fileViewUrl(file.id),
|
||||
mimeType: file.mimeType,
|
||||
});
|
||||
return (
|
||||
<Group
|
||||
justify="space-between"
|
||||
wrap="nowrap"
|
||||
p="sm"
|
||||
style={{ borderRadius: 12, border: `1px solid ${BORDER}` }}
|
||||
style={{
|
||||
borderRadius: 14,
|
||||
border: `1px solid ${BORDER}`,
|
||||
background: "#fff",
|
||||
transition: "border-color 120ms ease, box-shadow 120ms ease",
|
||||
}}
|
||||
className="hover:border-edr-green-3 hover:shadow-sm"
|
||||
>
|
||||
<Group gap={12} wrap="nowrap" style={{ minWidth: 0 }}>
|
||||
<FileText size={16} color={MUTED} style={{ flexShrink: 0 }} />
|
||||
<Box
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
flexShrink: 0,
|
||||
borderRadius: 10,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: `${color}14`,
|
||||
color,
|
||||
}}
|
||||
>
|
||||
<FileText size={16} />
|
||||
<Text fz={8} fw={800} mt={1} style={{ letterSpacing: "0.04em" }}>
|
||||
{ext}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box style={{ minWidth: 0 }}>
|
||||
<Text fz={14} fw={600} style={{ color: INK }} truncate>
|
||||
{kind}
|
||||
@@ -925,11 +1049,7 @@ function DocFileRow({
|
||||
</Box>
|
||||
</Group>
|
||||
<Group gap={8} wrap="nowrap">
|
||||
{isViewable({
|
||||
name: file.name,
|
||||
url: fileViewUrl(file.id),
|
||||
mimeType: file.mimeType,
|
||||
}) && (
|
||||
{viewable && (
|
||||
<Button
|
||||
variant="light"
|
||||
color="edr-green"
|
||||
|
||||
390
docs/freight-platform/SYSTEM-FLOW.md
Normal file
390
docs/freight-platform/SYSTEM-FLOW.md
Normal file
@@ -0,0 +1,390 @@
|
||||
# EDR Freight — How the System Works (Step by Step)
|
||||
|
||||
A plain-language walkthrough of the whole customer journey:
|
||||
|
||||
**Onboarding → Contract → Clearance → Booking → Schedule → Delivery**
|
||||
|
||||
Every step shows its branches. Read the arrows (`→`) as "then". Read **IF** blocks as the different paths.
|
||||
|
||||
---
|
||||
|
||||
## 1) Onboarding
|
||||
|
||||
> Goal: register the company so it can make bookings. The wizard has **9 steps** in this order.
|
||||
|
||||
```
|
||||
1. Nationality 2. Role/Operation 3. Company info 4. Personnel (GM)
|
||||
5. Contact person 6. Verify phone (OTP) 7. Power of Attorney (optional)
|
||||
8. Documents 9. Business license per profile
|
||||
```
|
||||
|
||||
### Step 1 — Pick nationality
|
||||
|
||||
```
|
||||
Foreign OR Ethiopian
|
||||
```
|
||||
(stored on the company: `nationality = "foreign" | "ethiopian"`)
|
||||
|
||||
### Step 2 — Pick operation type(s)
|
||||
|
||||
You may pick **more than one**. Each one becomes its own *profile* with its own approval + license.
|
||||
|
||||
```
|
||||
Importer OR Exporter OR Freight Forwarder
|
||||
(importer | exporter | freight_forwarder)
|
||||
```
|
||||
|
||||
### Steps 3–7 — Fill company + people
|
||||
|
||||
- **Company:** TIN (auto-looked-up from eTrade), name, email, phone, address (region/zone/woreda/kebele/house), VAT, FAN.
|
||||
- **Personnel:** General Manager name / email / phone.
|
||||
- **Contact person:** name / phone (+ optional position, email).
|
||||
- **Verify:** SMS OTP sent to the contact phone — must enter the 6-digit code.
|
||||
- **Power of Attorney:** all optional.
|
||||
|
||||
### Step 8 — Upload company documents → **THIS IS WHERE THE PATH SPLITS**
|
||||
|
||||
The required documents depend **only on nationality** (NOT on operation type).
|
||||
|
||||
```
|
||||
IF Ethiopian → upload:
|
||||
• TIN Certificate
|
||||
• Commercial License
|
||||
• National ID
|
||||
|
||||
IF Foreign → upload:
|
||||
• TIN Certificate
|
||||
• Investment License
|
||||
• National ID
|
||||
• Passport
|
||||
```
|
||||
|
||||
All are required (1 file each, pdf/jpg/png, ≤10 MB).
|
||||
|
||||
### Step 9 — Business license per profile
|
||||
|
||||
For **each** operation type you picked, upload that profile's business/trade license (1+ files each).
|
||||
|
||||
### After onboarding finishes
|
||||
|
||||
```
|
||||
Company status → "Pending" (backoffice must approve)
|
||||
Each profile status → "Pending"
|
||||
|
||||
Backoffice approves each profile one by one
|
||||
→ profile status = "active", gets a reference (e.g. IM-00001 / EX-00001)
|
||||
→ only then can that profile create contracts/bookings
|
||||
```
|
||||
|
||||
**Branch summary**
|
||||
|
||||
| Nationality | Company documents required |
|
||||
|-------------|----------------------------|
|
||||
| Ethiopian | TIN Certificate · Commercial License · National ID |
|
||||
| Foreign | TIN Certificate · Investment License · National ID · Passport |
|
||||
|
||||
> Operation type changes **nothing** in the document set — only adds one business-license card per profile.
|
||||
|
||||
---
|
||||
|
||||
## 2) Contract
|
||||
|
||||
> Goal: agree the terms (route, cargo, price) and sign. Only an **active profile** can do this.
|
||||
|
||||
### Create — the wizard (4 steps)
|
||||
|
||||
```
|
||||
Step 0 Setup operation direction (import/export/intercity),
|
||||
contract kind (ONE_TIME vs GENERAL),
|
||||
new vs renewal, service type, currency,
|
||||
first/last mile, customs-clearing on/off, equipment return
|
||||
Step 1 Cargo+Route container sizes OR bulk commodity, hazardous/reefer flags,
|
||||
origin & destination yard, extra routes (GENERAL only)
|
||||
Step 2 Documents required onboarding docs + any contract-specific uploads
|
||||
Step 3 Review check everything, see quotation, submit (or save draft)
|
||||
```
|
||||
|
||||
Two key choices made here decide later paths:
|
||||
|
||||
```
|
||||
contract kind: ONE_TIME (one shipment at a time)
|
||||
GENERAL (ship many times over a validity window)
|
||||
|
||||
customs clearing: ENABLED → Path B (Global Logistics clears for you)
|
||||
DISABLED → Path A (you self-clear) — for IMPORT/EXPORT
|
||||
(DOMESTIC/intercity → no clearance at all)
|
||||
```
|
||||
|
||||
### Status journey (happy path)
|
||||
|
||||
```
|
||||
DRAFT
|
||||
→ SUBMITTED (customer submits; prices frozen)
|
||||
→ PENDING_APPROVAL (staff accepts intake, sets validity window)
|
||||
→ APPROVED (approval chain signs: LINE_STAFF → DIRECTOR → CEO)
|
||||
→ CONTRACT_READY (staff generates the contract PDF)
|
||||
→ SIGNED_CUSTOMER (customer signs)
|
||||
→ counter-sign by staff/director/ceo … then it SPLITS ↓
|
||||
```
|
||||
|
||||
### The counter-sign split → which path?
|
||||
|
||||
```
|
||||
IF customs clearing ENABLED (IMPORT/EXPORT) → PATH B
|
||||
status → AWAITING_CLEARANCE_DOCUMENTS
|
||||
|
||||
IF customs clearing DISABLED (IMPORT/EXPORT) → PATH A (self-clear)
|
||||
status → AWAITING_CLEARANCE_DOCUMENTS
|
||||
|
||||
IF DOMESTIC / intercity (no clearance) → NO CLEARANCE
|
||||
status → CONTRACT_ACTIVE (GENERAL) or FULLY_EXECUTED (ONE_TIME)
|
||||
→ customer can book a shipment right away (skip to section 4)
|
||||
```
|
||||
|
||||
**Side branches at any review stage**
|
||||
|
||||
```
|
||||
staff requests changes → CHANGES_REQUESTED → customer edits → SUBMITTED again
|
||||
staff rejects → REJECTED
|
||||
customer/staff cancels → CANCELLED
|
||||
GENERAL contract later → renew → RENEWAL_DRAFT (copies the old contract)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3) Clearance
|
||||
|
||||
> Only happens for IMPORT/EXPORT contracts. Two paths. The loop is the same idea:
|
||||
> **customer uploads → reviewer approves or queries → customer re-uploads → … → finalize.**
|
||||
|
||||
### Who reviews?
|
||||
|
||||
```
|
||||
PATH A (self-clear, customs DISABLED) → reviewed by OPERATIONS team
|
||||
PATH B (customs, customs ENABLED) → reviewed by GLOBAL LOGISTICS (GL)
|
||||
```
|
||||
|
||||
### The status sub-states
|
||||
|
||||
```
|
||||
AWAITING_CLEARANCE_DOCUMENTS customer must upload
|
||||
CLEARANCE_UNDER_REVIEW reviewer is checking
|
||||
CLEARANCE_READY_FOR_BOOKING (Path B) done — GL will make the booking
|
||||
SELF_CLEARED (Path A) done — customer will make the booking
|
||||
```
|
||||
|
||||
### The review loop (both paths)
|
||||
|
||||
```
|
||||
1. Customer uploads all required documents
|
||||
→ status = CLEARANCE_UNDER_REVIEW
|
||||
→ each document = PENDING
|
||||
|
||||
2. Reviewer goes document by document:
|
||||
APPROVE → that document = APPROVED
|
||||
QUERY → that document = QUERIED (note required)
|
||||
→ contract drops back to AWAITING_CLEARANCE_DOCUMENTS
|
||||
(only the queried doc needs re-uploading; approved ones stay)
|
||||
|
||||
3. Customer re-uploads the queried document → back to step 2
|
||||
|
||||
4. When ALL required documents are APPROVED → finalize (below)
|
||||
```
|
||||
|
||||
### PATH A — self-clear (Operations)
|
||||
|
||||
```
|
||||
documents the CUSTOMER uploads (examples):
|
||||
import: customs declaration (IM4/IM5), import release, duty/tax receipt,
|
||||
delivery order, supporting doc
|
||||
export: customs declaration (EX3/EX8), export release, transit (T1), supporting doc
|
||||
|
||||
no output documents in Path A.
|
||||
|
||||
Operations finalize (POST .../clearance/ops-finalize)
|
||||
requires: every required doc APPROVED
|
||||
→ clearanceStatus = SELF_CLEARED
|
||||
→ contract status = CONTRACT_ACTIVE (GENERAL) or FULLY_EXECUTED (ONE_TIME)
|
||||
→ CUSTOMER creates the booking (section 4)
|
||||
```
|
||||
|
||||
### PATH B — customs (Global Logistics)
|
||||
|
||||
```
|
||||
documents the CUSTOMER uploads (examples):
|
||||
import container: commercial invoice, packing list, import license,
|
||||
certificate of origin, freight cost, bill of lading,
|
||||
VGM*, release order*
|
||||
export container: booking confirmation, invoice, packing list,
|
||||
shipping instruction, bank permit, export license,
|
||||
VGM letter*, railway bill, delegation letter
|
||||
(* = required)
|
||||
|
||||
then GL uploads OUTPUT documents (container only):
|
||||
import: IM4 (required), IM5 (optional), transit screenshot
|
||||
export: EX3 (required), EX8, export release, T1
|
||||
|
||||
GL finalize (POST .../clearance/finalize)
|
||||
requires: every required customer doc APPROVED
|
||||
AND every required output doc uploaded
|
||||
→ clearanceStatus = CLEARANCE_READY_FOR_BOOKING
|
||||
→ GL (not the customer) creates the booking (section 4)
|
||||
```
|
||||
|
||||
### Cycles (GENERAL contracts)
|
||||
|
||||
A **cycle** is one clearance round. ONE_TIME contracts have a single cycle (#1). GENERAL contracts open a new cycle each time they need clearance before the next shipment.
|
||||
|
||||
---
|
||||
|
||||
## 4) Booking
|
||||
|
||||
> Goal: turn a cleared/executed contract into an actual shipment. **Who creates it depends on the path.**
|
||||
|
||||
```
|
||||
PATH A / DOMESTIC → the CUSTOMER creates the booking
|
||||
PATH B (customs) → GLOBAL LOGISTICS creates the booking on the customer's behalf
|
||||
```
|
||||
|
||||
### The gate (who's allowed)
|
||||
|
||||
```
|
||||
IF contract has customs clearing (Path B):
|
||||
only GL, and only when clearanceStatus = CLEARANCE_READY_FOR_BOOKING
|
||||
|
||||
IF no customs (Path A / DOMESTIC):
|
||||
customer (or staff), and only when contract is FULLY_EXECUTED / CONTRACT_ACTIVE
|
||||
```
|
||||
|
||||
### Booking wizard (customer self-booking — 7 steps)
|
||||
|
||||
```
|
||||
0 Operation type import / export / intercity (+ FF variants)
|
||||
1 Contract type ONE_TIME vs GENERAL ; new vs renewal
|
||||
2 Service & mile service type, currency (USD/ETB), first/last mile,
|
||||
equipment return, customs agent / customs on-off
|
||||
3 Cargo details container list (type, qty, VGM) OR bulk weight,
|
||||
hazardous / refrigerated flags
|
||||
4 Route origin & destination yard;
|
||||
scheduledDate REQUIRED for ONE_TIME (estimate only),
|
||||
NOT set for GENERAL (a day is chosen later)
|
||||
5 Documents per-booking document uploads
|
||||
6 Review notes, submit
|
||||
```
|
||||
|
||||
### Booking status journey
|
||||
|
||||
```
|
||||
DRAFT
|
||||
→ generate price → SUBMITTED (if price changed: PRICE_CHANGED_PENDING_CONFIRM → confirm → SUBMITTED)
|
||||
→ PENDING_APPROVAL (staff accept intake)
|
||||
→ APPROVED / CONTRACT_READY (approval chain)
|
||||
→ SIGNED_CUSTOMER → counter-sign … SPLIT ↓
|
||||
|
||||
IF clearance applies → AWAITING_DOCUMENTS → DOCUMENTS_UNDER_REVIEW → CLEARANCE_READY
|
||||
IF no clearance → FULLY_EXECUTED directly
|
||||
```
|
||||
|
||||
> The booking has its **own** document clearance loop, mirroring the contract one
|
||||
> (upload → APPROVED/QUERIED → re-upload → finalize). Re-uploading a queried doc
|
||||
> resets it to PENDING. Booking proceeds only when all required docs are APPROVED.
|
||||
|
||||
### Pricing & payment
|
||||
|
||||
```
|
||||
price generated from rule engine + live rates, converted to chosen currency (USD/ETB)
|
||||
customer pays (Telebirr) once the booking is FULLY_EXECUTED / SELECTED_FOR_BATCH
|
||||
payment status: PENDING → VERIFICATION_IN_PROGRESS → PAID (or FAILED)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5) Schedule (Operations)
|
||||
|
||||
> Goal: put the booking on a train (or dispatch by road). Day-level pooling — the
|
||||
> customer picks a **day**, the batch engine assigns the actual **train** later.
|
||||
|
||||
```
|
||||
1. Customer requests operation pick a day that has an OPEN departure
|
||||
→ OPERATION_REQUEST_PENDING
|
||||
|
||||
2. Operations review the request → one of:
|
||||
ACCEPT → FULLY_EXECUTED (enters the train batch pool)
|
||||
(road service instead → ROAD_DISPATCH_PENDING, section 6)
|
||||
REQUEST_CHANGES → OPERATION_CHANGES_REQUESTED (note required; customer resubmits)
|
||||
ADJUST_PRICE → OPERATION_PRICE_PENDING_CONFIRM
|
||||
(customer confirms new price → pool, or rejects → changes requested)
|
||||
|
||||
3. Batch engine (cron) groups bookings by (origin yard, destination yard, day):
|
||||
allocates to open train schedules by priority score
|
||||
→ SELECTED_FOR_BATCH, assigns trainScheduleId, sets payment deadline
|
||||
|
||||
4. Payment (if not already paid) → PAID
|
||||
|
||||
5. IN_TRANSIT → COMPLETED
|
||||
```
|
||||
|
||||
### Wagon math
|
||||
|
||||
```
|
||||
wagons per booking = sum over containers of (qty × wagonsPerUnit), rounded up
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6) Delivery / Last mile
|
||||
|
||||
```
|
||||
IF road service:
|
||||
ACCEPT → ROAD_DISPATCH_PENDING (skips the train pool)
|
||||
billed by KM, dispatched by truck (First-Mile operations)
|
||||
|
||||
IF first/last mile chosen at booking:
|
||||
pickup + delivery addresses captured; equipment return = WITH / WITHOUT
|
||||
last-mile statuses: PAYMENT_PENDING → READY_TO_TRANSIT → IN_TRANSIT → RECEIVED_TO_PORT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The whole thing on one page
|
||||
|
||||
```
|
||||
ONBOARD
|
||||
nationality ─┬─ Ethiopian → TIN + Commercial License + National ID
|
||||
└─ Foreign → TIN + Investment License + National ID + Passport
|
||||
pick profiles (importer/exporter/FF) → upload license per profile
|
||||
→ backoffice approves profile → profile ACTIVE
|
||||
|
||||
CONTRACT
|
||||
wizard (setup → cargo+route → docs → review) → SUBMITTED
|
||||
→ staff accept → approval chain → CONTRACT_READY → customer sign → counter-sign
|
||||
→ SPLIT:
|
||||
customs ENABLED (import/export) → PATH B clearance
|
||||
customs DISABLED (import/export) → PATH A clearance
|
||||
DOMESTIC → no clearance, ready to book
|
||||
|
||||
CLEARANCE (import/export only) loop: upload → approve/query → re-upload → finalize
|
||||
PATH A: Operations review → SELF_CLEARED → CUSTOMER books
|
||||
PATH B: GL review + GL output docs → READY_FOR_BOOKING → GL books
|
||||
|
||||
BOOKING
|
||||
created by CUSTOMER (Path A / domestic) or GL (Path B)
|
||||
price → pay → (its own doc clearance if applicable) → ready to schedule
|
||||
|
||||
SCHEDULE
|
||||
request a day → Operations accept → batch engine → train assigned
|
||||
→ pay → IN_TRANSIT → COMPLETED
|
||||
(road service → ROAD_DISPATCH_PENDING → truck)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Where this lives in the code (quick map)
|
||||
|
||||
| Area | Key files |
|
||||
|------|-----------|
|
||||
| Onboarding | `portal/.../components/onboarding/OnboardingWizardDialog.tsx`, `api/.../companies/companies.service.ts`, `api/src/seed/file-upload-settings.seeder.ts` |
|
||||
| Contract | `portal/.../contracts/new-contract-form/`, `api/.../contracts/contract-transition.service.ts`, `entities/contract.entity.ts` |
|
||||
| Clearance | `api/.../contracts/contract-clearance.service.ts`, `contract-clearance.util.ts`, `portal/.../contracts/ContractClearancePanel.tsx` |
|
||||
| Booking | `portal/.../bookings/new-booking-form/`, `api/.../bookings/booking-transition.service.ts`, `contract-booking.service.ts` |
|
||||
| Schedule | `api/.../train-scheduling/booking-batch.service.ts`, `backoffice/.../operations/FirstMilePage.tsx` |
|
||||
Reference in New Issue
Block a user