From f1452ba67e98100d5eaa53e6224447493b8d4839 Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Thu, 9 Jul 2026 09:38:34 +0300 Subject: [PATCH 01/25] feat: ( forget password ) add phone number --- .../backoffice/src/app/login/page.tsx | 38 +++++++++---------- .../backoffice/src/lib/api/auth.ts | 6 ++- .../portal/src/app/forgot-password/page.tsx | 22 +++++------ .../portal/src/lib/api/auth.ts | 6 ++- 4 files changed, 38 insertions(+), 34 deletions(-) diff --git a/apps/edr-passenger-web/backoffice/src/app/login/page.tsx b/apps/edr-passenger-web/backoffice/src/app/login/page.tsx index f104db3da..c0d84cae8 100644 --- a/apps/edr-passenger-web/backoffice/src/app/login/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/login/page.tsx @@ -29,12 +29,12 @@ export default function LoginPage() { const [emailFocused, setEmailFocused] = useState(false); const [passwordFocused, setPasswordFocused] = useState(false); - const [view, setView] = useState<'login' | 'forgot'>('login'); - const [forgotEmail, setForgotEmail] = useState(''); - const [forgotLoading, setForgotLoading] = useState(false); - const [forgotError, setForgotError] = useState(''); - const [forgotSent, setForgotSent] = useState(false); - const [forgotFocused, setForgotFocused] = useState(false); + const [view, setView] = useState<'login' | 'forgot'>('login'); + const [forgotIdentifier, setForgotIdentifier] = useState(''); + const [forgotLoading, setForgotLoading] = useState(false); + const [forgotError, setForgotError] = useState(''); + const [forgotSent, setForgotSent] = useState(false); + const [forgotFocused, setForgotFocused] = useState(false); const router = useRouter(); const { login } = useAuthStore(); @@ -66,13 +66,13 @@ export default function LoginPage() { setForgotLoading(true); setForgotError(''); try { - await iamAuthApi.forgotPassword(forgotEmail); + await iamAuthApi.forgotPassword(forgotIdentifier.trim()); setForgotSent(true); } catch (err: any) { const msg = err.response?.data?.message || err.message || ''; setForgotError( msg === 'user_not_found' - ? 'No account found with that email address.' + ? 'No account found with that email or phone number.' : msg || 'Failed to send the reset link. Please try again.' ); } finally { @@ -209,7 +209,7 @@ export default function LoginPage() {
@@ -295,10 +295,10 @@ export default function LoginPage() { )}
- {/* Email field */} + {/* Email or phone field */}
{ setForgotEmail(e.target.value); setForgotError(''); }} + type="text" + value={forgotIdentifier} + onChange={(e) => { setForgotIdentifier(e.target.value); setForgotError(''); }} onFocus={() => setForgotFocused(true)} onBlur={() => setForgotFocused(false)} className="w-full px-4 py-3 rounded-xl bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-600 text-sm focus:outline-none" - placeholder="name@edr.com" + placeholder="name@edr.com or +251..." required - autoComplete="email" + autoComplete="username" />
@@ -322,9 +322,9 @@ export default function LoginPage() { {/* Submit */}
diff --git a/apps/edr-passenger-web/portal/src/lib/api/auth.ts b/apps/edr-passenger-web/portal/src/lib/api/auth.ts index 14e9a16a1..b37361238 100644 --- a/apps/edr-passenger-web/portal/src/lib/api/auth.ts +++ b/apps/edr-passenger-web/portal/src/lib/api/auth.ts @@ -8,8 +8,10 @@ const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'; // current password is wrong on change-password, and OTP failures must surface // as inline errors, not a logout. export const iamAuthApi = { - forgotPassword: (email: string) => - axios.post(`${API_URL}/v1/auth/forgot-password`, { email }), + // `identifier` can be an email address or a phone number — the IAM accepts + // either in the `email` field of the forgot-password body. + forgotPassword: (identifier: string) => + axios.post(`${API_URL}/v1/auth/forgot-password`, { email: identifier }), // Re-sends the registration verification code for a still-pending account. resendRegistrationCode: (data: { email: string; phoneNumber: string }) => From aa656eed7ed3934b8e31317f5e0c3462b197fac9 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Thu, 9 Jul 2026 06:40:45 +0000 Subject: [PATCH 02/25] fix: profile id and licence to registration --- .../companies/company-profile.repository.ts | 22 ++++- .../entities/company-profile.entity.ts | 2 +- .../src/seed/file-upload-settings.seeder.ts | 83 ++++++++++--------- .../pages/bookings/resubmit/resubmitDocs.ts | 2 +- .../src/pages/settings/NationalitySelect.tsx | 2 +- 5 files changed, 68 insertions(+), 43 deletions(-) diff --git a/apps/edr-freight-api/src/modules/companies/company-profile.repository.ts b/apps/edr-freight-api/src/modules/companies/company-profile.repository.ts index 15aec5ac6..9bf14cd61 100644 --- a/apps/edr-freight-api/src/modules/companies/company-profile.repository.ts +++ b/apps/edr-freight-api/src/modules/companies/company-profile.repository.ts @@ -1,4 +1,4 @@ -import { Injectable } from "@nestjs/common"; +import { Injectable, InternalServerErrorException } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; import { BaseRepository } from "@edr/api-common"; @@ -20,6 +20,11 @@ const PREFIX_MAP: Record = { [ProfileType.transporter]: "TR", }; +const SERIES_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + +/** Numbers per series letter: A00001..A99999, then B00001. */ +const SERIES_SIZE = 99_999; + @Injectable() export class CompanyProfileRepository extends BaseRepository { constructor( @@ -38,9 +43,20 @@ export class CompanyProfileRepository extends BaseRepository { const result = await this.repository.query( `SELECT nextval('${seqName}') AS next_id`, ); - const nextId = result[0].next_id as number; + const nextId = Number(result[0].next_id); + const offset = nextId - 1; + const seriesIndex = Math.floor(offset / SERIES_SIZE); + + if (seriesIndex >= SERIES_LETTERS.length) { + throw new InternalServerErrorException( + `Company profile reference series exhausted for type "${type}"`, + ); + } + + const letter = SERIES_LETTERS[seriesIndex]; + const number = (offset % SERIES_SIZE) + 1; const prefix = PREFIX_MAP[type]; - return `${prefix}-${String(nextId).padStart(5, "0")}`; + return `${prefix}-${letter}${String(number).padStart(5, "0")}`; } async findByCompanyId(companyId: string): Promise { diff --git a/apps/edr-freight-api/src/modules/companies/entities/company-profile.entity.ts b/apps/edr-freight-api/src/modules/companies/entities/company-profile.entity.ts index 72696766f..2266b0c65 100644 --- a/apps/edr-freight-api/src/modules/companies/entities/company-profile.entity.ts +++ b/apps/edr-freight-api/src/modules/companies/entities/company-profile.entity.ts @@ -60,7 +60,7 @@ export class CompanyProfile extends BaseEntity { type!: ProfileType; /** - * Official profile reference (e.g. "EX-00001"). Minted only when the profile + * Official profile reference (e.g. "EX-A00001"). Minted only when the profile * is approved (status → Active); pending/unapproved profiles carry NULL. * The unique index tolerates this because Postgres treats NULLs as distinct. * API responses surface it as "" when absent — see ResponseCompanyProfileDto. diff --git a/apps/edr-freight-api/src/seed/file-upload-settings.seeder.ts b/apps/edr-freight-api/src/seed/file-upload-settings.seeder.ts index 1aef33801..99dae5974 100644 --- a/apps/edr-freight-api/src/seed/file-upload-settings.seeder.ts +++ b/apps/edr-freight-api/src/seed/file-upload-settings.seeder.ts @@ -33,8 +33,9 @@ const ETHIOPIAN_ONBOARDING_FIELDS: OnboardingField[] = [ }, { fileKey: "commercial_license", - fileLabel: "Commercial License", - helpText: "Verified against the government trade system during registration.", + fileLabel: "Commercial Registration", + helpText: + "Verified against the government trade system during registration.", isRequired: true, isMultiple: false, maxFiles: 1, @@ -108,7 +109,8 @@ const LEGACY_ONBOARDING_FIELDS: OnboardingField[] = [ { fileKey: "business_license", fileLabel: "Business License / Trade License", - helpText: "Verified against the government trade system during registration.", + helpText: + "Verified against the government trade system during registration.", isRequired: true, isMultiple: false, maxFiles: 1, @@ -489,9 +491,14 @@ const SELF_CLEARANCE_SETTINGS: OnboardingDocumentSetting[] = [ const CONTRACT_INTAKE_ENTITY = "contract_intake"; const CONTRACT_INTAKE_FIELDS: OnboardingField[] = [ - clearanceField("commercial_framework", "Commercial Framework / Agreement", 1, { - required: false, - }), + clearanceField( + "commercial_framework", + "Commercial Framework / Agreement", + 1, + { + required: false, + }, + ), clearanceField("onboarding_attachment", "Onboarding Attachment", 2, { required: false, }), @@ -542,7 +549,7 @@ const DRIVER_DOCUMENT_SETTINGS: OnboardingDocumentSetting[] = [ export class FileUploadSettingsSeeder { private readonly logger = new Logger(FileUploadSettingsSeeder.name); - constructor(private readonly dataSource: DataSource) {} + constructor(private readonly dataSource: DataSource) { } async run() { await this.dataSource.transaction(async (manager) => { @@ -552,35 +559,35 @@ export class FileUploadSettingsSeeder { const allSettings: Array< OnboardingDocumentSetting & { description: string } > = [ - ...COMPANY_ONBOARDING_DOCUMENTS.map((s) => ({ - ...s, - description: COMPANY_ONBOARDING_DESCRIPTION, - })), - ...CLEARANCE_DOCUMENT_SETTINGS.map((s) => ({ - ...s, - description: CLEARANCE_DESCRIPTION, - })), - ...CONTRACT_CLEARANCE_SETTINGS.map((s) => ({ - ...s, - description: - "Pre-booking clearance documents collected on the contract (Path B), by operation and freight type.", - })), - ...SELF_CLEARANCE_SETTINGS.map((s) => ({ - ...s, - description: - "Customer self-clearance documents (Path A, no EDR customs service), reviewed by Operations.", - })), - ...CONTRACT_INTAKE_SETTINGS.map((s) => ({ - ...s, - description: - "Commercial/framework documents attached at contract submission.", - })), - ...DRIVER_DOCUMENT_SETTINGS.map((s) => ({ - ...s, - description: - "Documents uploaded against a driver profile (license, ID, contracts, etc.).", - })), - ]; + ...COMPANY_ONBOARDING_DOCUMENTS.map((s) => ({ + ...s, + description: COMPANY_ONBOARDING_DESCRIPTION, + })), + ...CLEARANCE_DOCUMENT_SETTINGS.map((s) => ({ + ...s, + description: CLEARANCE_DESCRIPTION, + })), + ...CONTRACT_CLEARANCE_SETTINGS.map((s) => ({ + ...s, + description: + "Pre-booking clearance documents collected on the contract (Path B), by operation and freight type.", + })), + ...SELF_CLEARANCE_SETTINGS.map((s) => ({ + ...s, + description: + "Customer self-clearance documents (Path A, no EDR customs service), reviewed by Operations.", + })), + ...CONTRACT_INTAKE_SETTINGS.map((s) => ({ + ...s, + description: + "Commercial/framework documents attached at contract submission.", + })), + ...DRIVER_DOCUMENT_SETTINGS.map((s) => ({ + ...s, + description: + "Documents uploaded against a driver profile (license, ID, contracts, etc.).", + })), + ]; for (const documentSetting of allSettings) { await settingRepository.upsert( @@ -601,7 +608,9 @@ export class FileUploadSettingsSeeder { }); if (!setting) { - throw new Error(`file_upload_setting_seed_failed:${documentSetting.code}`); + throw new Error( + `file_upload_setting_seed_failed:${documentSetting.code}`, + ); } await fieldRepository.delete({ settingId: setting.id }); diff --git a/apps/edr-freight-web/portal/src/pages/bookings/resubmit/resubmitDocs.ts b/apps/edr-freight-web/portal/src/pages/bookings/resubmit/resubmitDocs.ts index d8b660413..0a2f1a30f 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/resubmit/resubmitDocs.ts +++ b/apps/edr-freight-web/portal/src/pages/bookings/resubmit/resubmitDocs.ts @@ -10,7 +10,7 @@ const LABEL_BY_CODE = new Map([ ...REQUIRED_DOC_FIELDS.map((d) => [d.key, d.label] as const), // Company onboarding document codes (see file-upload-settings seeder). ["tin_certificate", "TIN Certificate"], - ["commercial_license", "Commercial License"], + ["commercial_license", "Commercial Registration"], ["business_license", "Business License / Trade License"], ["investment_license", "Investment License"], ["national_id", "National ID"], diff --git a/apps/edr-freight-web/portal/src/pages/settings/NationalitySelect.tsx b/apps/edr-freight-web/portal/src/pages/settings/NationalitySelect.tsx index 5b779847a..2115ec083 100644 --- a/apps/edr-freight-web/portal/src/pages/settings/NationalitySelect.tsx +++ b/apps/edr-freight-web/portal/src/pages/settings/NationalitySelect.tsx @@ -26,7 +26,7 @@ export default function NationalitySelect({ } selected={value === "ethiopian"} onClick={() => onChange("ethiopian")} From 66edeb84bf1c0f14bbbcee69894618767dedb302 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Thu, 9 Jul 2026 07:02:02 +0000 Subject: [PATCH 03/25] style: update the chat widget --- .../src/features/support/SupportWidget.tsx | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/apps/edr-passenger-web/portal/src/features/support/SupportWidget.tsx b/apps/edr-passenger-web/portal/src/features/support/SupportWidget.tsx index 2b46a97b6..96ecc7276 100644 --- a/apps/edr-passenger-web/portal/src/features/support/SupportWidget.tsx +++ b/apps/edr-passenger-web/portal/src/features/support/SupportWidget.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Headset, MessageCircle } from 'lucide-react'; +import { Headset } from 'lucide-react'; import { useState } from 'react'; import { SupportPanel } from './SupportPanel'; @@ -8,6 +8,7 @@ import { useUnreadCount } from './useSupport'; import { useSupportSocket } from './useSupportSocket'; const GREEN = 'rgb(20 113 76)'; +const GRADIENT = `linear-gradient(135deg, ${GREEN}, rgb(30 140 96))`; /** * Floating support launcher for the portal. One device-scoped thread — opens @@ -28,10 +29,27 @@ export function SupportWidget() { @@ -969,24 +960,13 @@ export default function ResultsPage() { {renderClassModal()}
-
-
- +
+
+ + No trains available on {requestedDateLabel}.
-

- No trains available -

-

- No trains are available on{" "} - - {requestedDateLabel} - -

-
@@ -1017,22 +997,13 @@ export default function ResultsPage() { return (
-
-
-
- +
+
+
+ + No trains found matching your search. Try adjusting your dates or route.
-

- No trains found -

-

- We couldn't find any trains matching your search criteria.{" "} -
Try adjusting your dates or route. -

-
@@ -1156,29 +1127,13 @@ export default function ResultsPage() { {outboundSchedules.length === 0 && alternativeOutbound.length > 0 && (
-
-
- +
+
+ + No trains on {requestedDate ? format(new Date(`${requestedDate}T00:00:00`), "EEEE, MMMM d") : "your selected date"}.
-

- No trains available -

-

- No trains are available on{" "} - - {requestedDate - ? format( - new Date(`${requestedDate}T00:00:00`), - "EEEE, MMMM d, yyyy", - ) - : "your selected date"} - -

-
@@ -1256,29 +1211,13 @@ export default function ResultsPage() { {inboundSchedules.length === 0 && alternativeInbound.length > 0 && (
-
-
- +
+
+ + No trains on {requestedReturnDate ? format(new Date(`${requestedReturnDate}T00:00:00`), "EEEE, MMMM d") : "your selected return date"}.
-

- No trains available -

-

- No trains are available on{" "} - - {requestedReturnDate - ? format( - new Date(`${requestedReturnDate}T00:00:00`), - "EEEE, MMMM d, yyyy", - ) - : "your selected return date"} - -

-
From 758c6082f3ab6353ceb73bded525d4897f634c12 Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Thu, 9 Jul 2026 11:15:42 +0300 Subject: [PATCH 07/25] Passenger portal build issue resolution --- .../portal/src/app/booking/results/page.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx index aa1bf4279..3ef1c8875 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx @@ -203,11 +203,6 @@ export default function ResultsPage() { isRoundTrip && !!results && inboundSchedules.length === 0 ? results?.alternativeInbound || results?.inboundAlternatives || [] : []; - const requestedDate: string = - (results && results.requestedDate) || searchData.date; - const requestedReturnDate: string = - (results && results.requestedReturnDate) || searchData.returnDate || ""; - const isOneWayNoOutbound = !isRoundTrip && !!results && outboundSchedules.length === 0; // Round-trip: show results view if either leg has exact results OR alternatives. @@ -965,9 +960,6 @@ export default function ResultsPage() { } if (isOneWayNoOutbound) { - const requestedDateLabel = requestedDate - ? format(new Date(`${requestedDate}T00:00:00`), "EEEE, MMMM d, yyyy") - : "your selected date"; const hasAlternatives = alternativeOutbound.length > 0; return ( From 5a8db998e513105838e73b63c2d53743c51c3810 Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Thu, 9 Jul 2026 11:23:14 +0300 Subject: [PATCH 08/25] Alternative schedules notification updates --- .../portal/src/app/booking/results/page.tsx | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx index 3ef1c8875..33609ba0e 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx @@ -944,10 +944,11 @@ export default function ResultsPage() { return (
-
-
-
- +
+
+
+ + No trains found for your selected dates or route.
+ + setOpened(false)} + title="Send a password-reset code" + centered + > + + + We'll send a one-time code to this customer's primary contact. + They choose their own new password — you will not see it. + + + setChannel(v as ResetChannel)} + label="Send the code via" + > + + + + + + + + The code goes to the primary contact's own email or phone, which + may differ from the company contact details shown above. + + + + + + + ); +} diff --git a/apps/edr-freight-web/backoffice/src/components/customers/index.ts b/apps/edr-freight-web/backoffice/src/components/customers/index.ts index 2a87e3b0c..d42673d2b 100644 --- a/apps/edr-freight-web/backoffice/src/components/customers/index.ts +++ b/apps/edr-freight-web/backoffice/src/components/customers/index.ts @@ -13,5 +13,9 @@ export { ChangeRequestReview, ChangeRequestPendingBadge, } from "./ChangeRequestReview"; +export { + default as ResetPasswordAction, + type ResetPasswordActionProps, +} from "./ResetPasswordAction"; export { formatBytes, formatDate, formatMoney, humanize } from "./format"; export { TableCard, type TableCardProps } from "./TableCard"; diff --git a/apps/edr-freight-web/backoffice/src/constants/URLS.ts b/apps/edr-freight-web/backoffice/src/constants/URLS.ts index 77a3bc95e..c6f98c8a2 100644 --- a/apps/edr-freight-web/backoffice/src/constants/URLS.ts +++ b/apps/edr-freight-web/backoffice/src/constants/URLS.ts @@ -86,6 +86,8 @@ export const URL_CONSTANTS = { `/bookings/by-company/${id}/customer-view`, PAYMENTS_CUSTOMER_VIEW: (id: string) => `/payments/by-company/${id}/customer-view`, + RESET_PASSWORD: (companyId: string) => + `/backoffice/customers/${companyId}/reset-password`, }, BILLING: { diff --git a/apps/edr-freight-web/backoffice/src/lib/permissions.ts b/apps/edr-freight-web/backoffice/src/lib/permissions.ts index c8bfa7c84..a7d8c16c1 100644 --- a/apps/edr-freight-web/backoffice/src/lib/permissions.ts +++ b/apps/edr-freight-web/backoffice/src/lib/permissions.ts @@ -62,6 +62,7 @@ export const FREIGHT_PERMS = { update: "edr_freight_app:customers:update", deactivate: "edr_freight_app:customers:deactivate", verify: "edr_freight_app:customers:verify", + resetPassword: "edr_freight_app:customers:reset-password", }, payments: { view: "edr_freight_app:payments:view", diff --git a/apps/edr-freight-web/backoffice/src/pages/customers/CustomerDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/customers/CustomerDetailPage.tsx index c68a4ca25..317f915fb 100644 --- a/apps/edr-freight-web/backoffice/src/pages/customers/CustomerDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/customers/CustomerDetailPage.tsx @@ -43,6 +43,7 @@ import { ProfileChips, ProfileStatusBadge, ProfileTypeBadge, + ResetPasswordAction, TableCard, formatBytes, formatDate, @@ -573,6 +574,7 @@ export default function CustomerDetailPage() { } + action={} /> diff --git a/apps/edr-freight-web/backoffice/src/services/api.ts b/apps/edr-freight-web/backoffice/src/services/api.ts index 7710e077f..4f04e3358 100644 --- a/apps/edr-freight-web/backoffice/src/services/api.ts +++ b/apps/edr-freight-web/backoffice/src/services/api.ts @@ -12,6 +12,8 @@ import type { CustomerPayment, PaginatedCompanies, ProfileStatus, + ResetChannel, + ResetPasswordResult, } from "@/types/customer"; import { CreateDropdownOptionDto, @@ -2261,6 +2263,16 @@ export const api = { ({ id }) => QUERY_KEYS.CUSTOMERS.payments(id), ), + resetPassword: endpoint< + { companyId: string; channel: ResetChannel }, + ResetPasswordResult + >( + "customers", + "resetPassword", + ({ companyId, channel }) => + customersService.resetPassword(companyId, channel), + ), + setProfileStatus: endpoint< { profileId: string; status: ProfileStatus; note?: string }, CompanyProfile diff --git a/apps/edr-freight-web/backoffice/src/services/customers.service.ts b/apps/edr-freight-web/backoffice/src/services/customers.service.ts index cafe0aece..0476d41bb 100644 --- a/apps/edr-freight-web/backoffice/src/services/customers.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/customers.service.ts @@ -11,6 +11,8 @@ import type { CustomerPayment, PaginatedCompanies, ProfileStatus, + ResetChannel, + ResetPasswordResult, } from "@/types/customer"; const cleanParams = (params: object) => @@ -81,6 +83,22 @@ export const customersService = { .then((r) => r.data); }, + /** + * Send a password-reset code to the company's primary contact. Staff never + * receive a credential — the customer sets their own password from the code. + */ + resetPassword( + companyId: string, + channel: ResetChannel, + ): Promise { + return apiClient + .post( + URL_CONSTANTS.COMPANIES.RESET_PASSWORD(companyId), + { channel }, + ) + .then((r) => r.data); + }, + setProfileStatus( profileId: string, status: ProfileStatus, diff --git a/apps/edr-freight-web/backoffice/src/types/customer.ts b/apps/edr-freight-web/backoffice/src/types/customer.ts index 8d76571d4..7328decf0 100644 --- a/apps/edr-freight-web/backoffice/src/types/customer.ts +++ b/apps/edr-freight-web/backoffice/src/types/customer.ts @@ -99,6 +99,15 @@ export interface CompanyChangeRequest { updatedAt: string; } +/** The channel a customer's password-reset code is delivered over. */ +export type ResetChannel = "email" | "phone"; + +export interface ResetPasswordResult { + channel: ResetChannel; + /** Where the code went, e.g. `+251•••4821` — safe to show to staff. */ + maskedTarget: string; +} + /** Mirrors backend `Company` (+ its `companyProfiles`). */ export interface Company { id: string; diff --git a/apps/edr-freight-web/portal/src/App.tsx b/apps/edr-freight-web/portal/src/App.tsx index 697c2f89c..e4c5685fd 100644 --- a/apps/edr-freight-web/portal/src/App.tsx +++ b/apps/edr-freight-web/portal/src/App.tsx @@ -32,6 +32,7 @@ import EDRFreightLandingPage from "./pages/EDRFreightLandingPage"; import MyPortalPage from "./pages/MyPortalPage"; import MySignaturePage from "./pages/MySignaturePage"; import SettingsPage from "./pages/SettingsPage"; +import ForgotPasswordPage from "./pages/accounts/ForgotPasswordPage"; import LoginPage from "./pages/accounts/LoginPage"; import SetPasswordPage from "./pages/accounts/SetPasswordPage"; import SignupPage from "./pages/accounts/SignupPage"; @@ -252,6 +253,7 @@ const App = () => { }> } /> } /> + } /> {/* Signup-flow pages; reached while a session already exists */} diff --git a/apps/edr-freight-web/portal/src/components/auth/OtpChannelStep.tsx b/apps/edr-freight-web/portal/src/components/auth/OtpChannelStep.tsx new file mode 100644 index 000000000..b61bb2eab --- /dev/null +++ b/apps/edr-freight-web/portal/src/components/auth/OtpChannelStep.tsx @@ -0,0 +1,179 @@ +import { Alert, Button, PinInput, SegmentedControl, Stack, Text } from "@mantine/core"; +import { + AlertCircle, + ArrowLeft, + Mail, + RotateCw, + ShieldCheck, + Smartphone, +} from "lucide-react"; + +import { maskEmail, maskPhone } from "@/utils/identifier"; + +export type OtpChannel = "phone" | "email"; + +export const OTP_LENGTH = 6; + +export interface OtpChannelSelectProps { + value: OtpChannel; + onChange: (channel: OtpChannel) => void; + disabled?: boolean; + label?: string; +} + +/** Phone/email toggle deciding where the verification code is sent. */ +export function OtpChannelSelect({ + value, + onChange, + disabled, + label = "Send verification code via", +}: OtpChannelSelectProps) { + return ( +
+ + {label} + + onChange(v as OtpChannel)} + data={[ + { + value: "phone", + label: ( + + Phone + + ), + }, + { + value: "email", + label: ( + + Email + + ), + }, + ]} + /> +
+ ); +} + +export interface OtpChannelStepProps { + channel: OtpChannel; + /** Raw email or phone the code went to; masked before display. */ + target: string; + value: string; + onChange: (otp: string) => void; + onVerify: () => void; + onBack: () => void; + onResend: () => void; + /** Seconds until resend is allowed; 0 enables the button. */ + resendIn: number; + sending: boolean; + verifying: boolean; + error: string | null; + title?: string; + description?: string; + submitLabel: string; +} + +/** + * The "enter the code we sent you" stage. Shared by signup and the + * forgot-password flow — both send through the same `/api/otp/*` service. + */ +export default function OtpChannelStep({ + channel, + target, + value, + onChange, + onVerify, + onBack, + onResend, + resendIn, + sending, + verifying, + error, + title, + description, + submitLabel, +}: OtpChannelStepProps) { + const maskedTarget = channel === "email" ? maskEmail(target) : maskPhone(target); + const busy = sending || verifying; + + return ( + +
+ + + +
+ +
+

+ {title ?? `Verify your ${channel === "email" ? "email" : "phone"}`} +

+

+ We sent a {OTP_LENGTH}-digit code to{" "} + {maskedTarget}.{" "} + {description ?? "Enter it to continue."} +

+
+ + {error ? ( + }> + {error} + + ) : null} + + + + Verification code + + + + + + +
+ + +
+
+ ); +} diff --git a/apps/edr-freight-web/portal/src/components/auth/PasswordChecklist.tsx b/apps/edr-freight-web/portal/src/components/auth/PasswordChecklist.tsx new file mode 100644 index 000000000..89cea7a0e --- /dev/null +++ b/apps/edr-freight-web/portal/src/components/auth/PasswordChecklist.tsx @@ -0,0 +1,41 @@ +import { Check, X } from "lucide-react"; + +import { passwordRequirements } from "@/utils/passwordSchema"; + +export interface PasswordChecklistProps { + /** The current password value; the checklist hides itself when empty. */ + value: string; +} + +/** Live pass/fail list of the password rules, shown under a password field. */ +export default function PasswordChecklist({ value }: PasswordChecklistProps) { + if (!value) return null; + + return ( +
+ {passwordRequirements.map((req) => { + const met = req.test(value); + return ( +
+ + {met ? ( + + ) : ( + + )} + + + {req.label} + +
+ ); + })} +
+ ); +} diff --git a/apps/edr-freight-web/portal/src/constants/URLS.ts b/apps/edr-freight-web/portal/src/constants/URLS.ts index 89a3d57e7..5dfda7e58 100644 --- a/apps/edr-freight-web/portal/src/constants/URLS.ts +++ b/apps/edr-freight-web/portal/src/constants/URLS.ts @@ -5,6 +5,8 @@ export const URL_CONSTANTS = { REFRESH_TOKEN: "/api/auth/refresh-token", LOGOUT: "/api/auth/logout", PROFILE: "/auth/profile", + FORGOT_PASSWORD_REQUEST: "/api/auth/forgot-password/request", + FORGOT_PASSWORD_VERIFY: "/api/auth/forgot-password/verify", }, USERS: { diff --git a/apps/edr-freight-web/portal/src/hooks/useResendCooldown.ts b/apps/edr-freight-web/portal/src/hooks/useResendCooldown.ts new file mode 100644 index 000000000..deeca8125 --- /dev/null +++ b/apps/edr-freight-web/portal/src/hooks/useResendCooldown.ts @@ -0,0 +1,24 @@ +import { useEffect, useState } from "react"; + +/** Seconds a user must wait before another OTP can be requested. */ +const DEFAULT_COOLDOWN_SECONDS = 60; + +/** + * Countdown that gates the "Resend code" button. Ticks with setTimeout rather + * than wall-clock arithmetic, so it needs no Date.now(). + */ +export function useResendCooldown(seconds: number = DEFAULT_COOLDOWN_SECONDS) { + const [secondsLeft, setSecondsLeft] = useState(0); + + useEffect(() => { + if (secondsLeft <= 0) return; + const t = setTimeout(() => setSecondsLeft((s) => s - 1), 1000); + return () => clearTimeout(t); + }, [secondsLeft]); + + return { + secondsLeft, + start: () => setSecondsLeft(seconds), + reset: () => setSecondsLeft(0), + }; +} diff --git a/apps/edr-freight-web/portal/src/pages/accounts/ForgotPasswordPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/ForgotPasswordPage.tsx new file mode 100644 index 000000000..58cfeab43 --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/accounts/ForgotPasswordPage.tsx @@ -0,0 +1,312 @@ +import { type FormEvent, useState } from "react"; +import { Alert, Button, PasswordInput, Stack, TextInput } from "@mantine/core"; +import { AlertCircle, ArrowLeft, ArrowRight, KeyRound } from "lucide-react"; +import { Link, useNavigate } from "react-router-dom"; + +import { useResendCooldown } from "@/hooks/useResendCooldown"; +import AuthShell from "@/components/auth/AuthShell"; +import OtpChannelStep, { + OTP_LENGTH, + OtpChannelSelect, + type OtpChannel, +} from "@/components/auth/OtpChannelStep"; +import PasswordChecklist from "@/components/auth/PasswordChecklist"; +import { api } from "@/services/api"; +import type { ResetTicket } from "@/types/auth"; +import { normaliseIdentifier } from "@/utils/identifier"; +import { meetsAllRequirements } from "@/utils/passwordSchema"; +import { extractApiError } from "@/utils/result"; + +type Stage = "identify" | "otp" | "password"; + +export default function ForgotPasswordPage() { + const navigate = useNavigate(); + + const [stage, setStage] = useState("identify"); + const [identifier, setIdentifier] = useState(""); + const [channel, setChannel] = useState("phone"); + const [otpCode, setOtpCode] = useState(""); + // The reset ticket lives in memory only — persisting it would leave a + // password-change credential sitting in localStorage. + const [ticket, setTicket] = useState(null); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + + const [sending, setSending] = useState(false); + const [verifying, setVerifying] = useState(false); + const [error, setError] = useState(null); + const resendCooldown = useResendCooldown(); + + /** The identifier as the API will see it — normalised once, reused everywhere. */ + const normalised = normaliseIdentifier(identifier); + + const sendCode = async () => { + await api.auth.requestPasswordReset.call({ identifier: normalised, channel }); + setOtpCode(""); + resendCooldown.start(); + }; + + // Stage 1 — ask for a code. The API answers identically for unknown accounts, + // so we always advance; a non-existent identifier simply never receives a code. + const handleIdentify = async (event: FormEvent) => { + event.preventDefault(); + setError(null); + setSending(true); + try { + await sendCode(); + setStage("otp"); + } catch (err) { + setError(extractApiError(err).message); + } finally { + setSending(false); + } + }; + + const handleResend = async () => { + setError(null); + setSending(true); + try { + await sendCode(); + } catch (err) { + setError(extractApiError(err).message); + } finally { + setSending(false); + } + }; + + // Stage 2 — trade the code for a single-use ticket. + const handleVerify = async () => { + setError(null); + if (otpCode.trim().length !== OTP_LENGTH) { + setError(`Enter the ${OTP_LENGTH}-digit code we sent you.`); + return; + } + setVerifying(true); + try { + const result = await api.auth.verifyPasswordResetOtp.call({ + identifier: normalised, + channel, + otp: otpCode.trim(), + }); + setTicket(result); + setStage("password"); + } catch (err) { + setError(extractApiError(err).message); + } finally { + setVerifying(false); + } + }; + + // Stage 3 — spend the ticket on IAM's set-password. + const handleReset = async (event: FormEvent) => { + event.preventDefault(); + setError(null); + + if (!ticket) { + setError("Your reset session expired. Start again."); + setStage("identify"); + return; + } + if (password !== confirmPassword) { + setError("Passwords do not match."); + return; + } + + setVerifying(true); + try { + await api.auth.resetPassword.call({ + userId: ticket.userId, + // The API matches this against email / username / phone, so the typed + // identifier works regardless of which one it is. + email: normalised, + verificationCode: ticket.verificationCode, + newPassword: password, + confirmPassword, + }); + navigate("/login", { + replace: true, + state: { passwordReset: true }, + }); + } catch (err) { + setError(extractApiError(err).message); + } finally { + setVerifying(false); + } + }; + + const identifierLabel = + channel === "email" ? "the email on your account" : "the phone on your account"; + + return ( + +
+ {stage === "identify" ? ( +
+
+ + + +
+ +
+

+ Forgot your password? +

+

+ Enter your email or phone number and we'll send you a code to + reset it. +

+
+ + + setIdentifier(event.target.value)} + /> + + + +

+ The code goes to {identifierLabel}, which may differ from what you + typed above. +

+ + {error ? ( + }> + {error} + + ) : null} + + + +

+ Remembered it?{" "} + + Back to sign in + +

+
+
+ ) : null} + + {stage === "otp" ? ( + { + setStage("identify"); + setError(null); + }} + onResend={handleResend} + resendIn={resendCooldown.secondsLeft} + sending={sending} + verifying={verifying} + error={error} + title="Enter your reset code" + description="Enter it to choose a new password." + submitLabel="Verify code" + /> + ) : null} + + {stage === "password" ? ( +
+
+

+ Choose a new password +

+

+ Pick something strong you haven't used before. +

+
+ + +
+ setPassword(event.target.value)} + /> + +
+ + setConfirmPassword(event.target.value)} + /> + + {error ? ( + }> + {error} + + ) : null} + + + + +
+
+ ) : null} +
+
+ ); +} diff --git a/apps/edr-freight-web/portal/src/pages/accounts/LoginPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/LoginPage.tsx index 885888a05..7e8e512dd 100644 --- a/apps/edr-freight-web/portal/src/pages/accounts/LoginPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/LoginPage.tsx @@ -5,21 +5,11 @@ import { Link, useLocation, useNavigate } from "react-router-dom"; import useAuth from "@/hooks/useAuth"; import AuthShell from "@/components/auth/AuthShell"; +import { normaliseIdentifier } from "@/utils/identifier"; import { extractApiError } from "@/utils/result"; const EDR_LOGO = "/assets/edr-logo.png"; -/** Normalise Ethiopian local phone (09…/07…) to E.164; pass email through unchanged. */ -function normaliseIdentifier(raw: string): string { - const v = raw.trim(); - const digits = v.replace(/\D/g, ""); - if (digits.length >= 9 && (v.startsWith("0") || v.startsWith("+251"))) { - const local = digits.startsWith("251") ? digits.slice(3) : digits.replace(/^0/, ""); - return `+251${local}`; - } - return v.toLowerCase(); -} - export default function LoginPage() { const navigate = useNavigate(); const location = useLocation(); @@ -80,7 +70,7 @@ export default function LoginPage() {
Password Forgot password? diff --git a/apps/edr-freight-web/portal/src/pages/accounts/SetPasswordPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/SetPasswordPage.tsx index 87702bdd8..ee8dd1922 100644 --- a/apps/edr-freight-web/portal/src/pages/accounts/SetPasswordPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/SetPasswordPage.tsx @@ -8,30 +8,20 @@ import { z } from "zod"; import useAuth from "@/hooks/useAuth"; import AuthLayout from "@/components/auth/AuthLayout"; - -const passwordRequirements = [ - { label: "At least 8 characters", test: (v: string) => v.length >= 8 }, - { label: "One uppercase letter", test: (v: string) => /[A-Z]/.test(v) }, - { label: "One lowercase letter", test: (v: string) => /[a-z]/.test(v) }, - { label: "One number", test: (v: string) => /\d/.test(v) }, - { label: "One special character", test: (v: string) => /[^A-Za-z0-9]/.test(v) }, -] as const; +import { + PASSWORD_MISMATCH, + confirmPasswordField, + passwordField, + passwordRequirements, + samePassword, +} from "@/utils/passwordSchema"; const passwordSchema = z .object({ - password: z - .string() - .min(8, "Password must be at least 8 characters") - .regex(/[A-Z]/, "Password must include an uppercase letter") - .regex(/[a-z]/, "Password must include a lowercase letter") - .regex(/\d/, "Password must include a number") - .regex(/[^A-Za-z0-9]/, "Password must include a special character"), - confirmPassword: z.string().min(1, "Please confirm your password"), + password: passwordField, + confirmPassword: confirmPasswordField, }) - .refine((data) => data.password === data.confirmPassword, { - message: "Passwords do not match", - path: ["confirmPassword"], - }); + .refine(samePassword, PASSWORD_MISMATCH); type FormData = z.infer; diff --git a/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx index 5746eefd5..fe2fb09cd 100644 --- a/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx @@ -1,50 +1,39 @@ -import { useEffect, useState } from "react"; +import { useState } from "react"; import { zodResolver } from "@hookform/resolvers/zod"; import { Alert, Button, PasswordInput, - PinInput, - SegmentedControl, SimpleGrid, Stack, - Text, TextInput, } from "@mantine/core"; -import { - AlertCircle, - ArrowLeft, - ArrowRight, - Check, - Mail, - RotateCw, - ShieldCheck, - Smartphone, - X, -} from "lucide-react"; +import { AlertCircle, ArrowRight } from "lucide-react"; import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; import { z } from "zod"; import { userType } from "@/enums/userType"; import useAuth from "@/hooks/useAuth"; +import { useResendCooldown } from "@/hooks/useResendCooldown"; import type { SignupPayload } from "@/types/auth"; import AuthShell from "@/components/auth/AuthShell"; +import OtpChannelStep, { + OTP_LENGTH, + OtpChannelSelect, + type OtpChannel, +} from "@/components/auth/OtpChannelStep"; +import PasswordChecklist from "@/components/auth/PasswordChecklist"; import { ControlledPhoneField, isValidPhone } from "@/components/PhoneField"; import { api } from "@/services/api"; +import { + PASSWORD_MISMATCH, + confirmPasswordField, + passwordField, + samePassword, +} from "@/utils/passwordSchema"; import { extractApiError } from "@/utils/result"; -const passwordRequirements = [ - { label: "At least 8 characters", test: (v: string) => v.length >= 8 }, - { label: "One uppercase letter", test: (v: string) => /[A-Z]/.test(v) }, - { label: "One lowercase letter", test: (v: string) => /[a-z]/.test(v) }, - { label: "One number", test: (v: string) => /\d/.test(v) }, - { - label: "One special character", - test: (v: string) => /[^A-Za-z0-9]/.test(v), - }, -] as const; - const userSchema = z .object({ email: z.string().email("Invalid email address"), @@ -61,43 +50,20 @@ const userSchema = z en: z.string().min(2, "Name is required"), am: z.string().nullable(), }), - password: z - .string() - .min(8, "Password must be at least 8 characters") - .regex(/[A-Z]/, "Password must include an uppercase letter") - .regex(/[a-z]/, "Password must include a lowercase letter") - .regex(/\d/, "Password must include a number") - .regex(/[^A-Za-z0-9]/, "Password must include a special character"), - confirmPassword: z.string().min(1, "Please confirm your password"), + password: passwordField, + confirmPassword: confirmPasswordField, }) - .refine((data) => data.password === data.confirmPassword, { - message: "Passwords do not match", - path: ["confirmPassword"], - }); + .refine(samePassword, PASSWORD_MISMATCH); type FormData = z.infer; -/** Mask all but the first 7 chars of an E.164 phone for display. */ -const maskPhone = (p: string) => - p.length > 4 ? `${p.slice(0, 7)}${"*".repeat(Math.max(0, p.length - 7))}` : p; - -/** Mask the local part of an email for display (j***e@example.com). */ -const maskEmail = (email: string) => { - const [local, domain] = email.split("@"); - if (!local || !domain) return email; - if (local.length <= 2) return `${local[0] ?? ""}***@${domain}`; - return `${local[0]}***${local[local.length - 1]}@${domain}`; -}; - -type OtpChannel = "phone" | "email"; - export default function SignupPage() { const navigate = useNavigate(); const { signup } = useAuth(); const [error, setError] = useState(null); - // Two-stage signup: fill the form, then a mandatory SMS OTP challenge on the - // phone number before the account is actually created. The account is only + // Two-stage signup: fill the form, then a mandatory OTP challenge on the + // chosen channel before the account is actually created. The account is only // created after the code is verified — the OTP is a hard requirement. const [stage, setStage] = useState<"form" | "otp">("form"); const [pendingData, setPendingData] = useState(null); @@ -109,14 +75,7 @@ export default function SignupPage() { const [verifying, setVerifying] = useState(false); const [otpCode, setOtpCode] = useState(""); const [otpError, setOtpError] = useState(null); - const [resendIn, setResendIn] = useState(0); - - // Resend cooldown countdown (pure setTimeout ticks — no Date.now needed). - useEffect(() => { - if (resendIn <= 0) return; - const t = setTimeout(() => setResendIn((s) => s - 1), 1000); - return () => clearTimeout(t); - }, [resendIn]); + const resendCooldown = useResendCooldown(); const { register, @@ -170,7 +129,7 @@ export default function SignupPage() { setOtpChannel(channel); setOtpCode(""); setOtpError(null); - setResendIn(60); + resendCooldown.start(); setStage("otp"); } catch (err) { setError(extractApiError(err).message); @@ -190,7 +149,7 @@ export default function SignupPage() { : { phone: pendingData.phone }, ); setOtpCode(""); - setResendIn(60); + resendCooldown.start(); } catch (err) { setOtpError(extractApiError(err).message); } finally { @@ -202,8 +161,8 @@ export default function SignupPage() { const confirmOtp = async () => { if (!pendingData) return; setOtpError(null); - if (otpCode.trim().length !== 6) { - setOtpError("Enter the 6-digit code we sent you."); + if (otpCode.trim().length !== OTP_LENGTH) { + setOtpError(`Enter the ${OTP_LENGTH}-digit code we sent you.`); return; } setVerifying(true); @@ -298,35 +257,11 @@ export default function SignupPage() { disabled={sending} /> -
- - Send verification code via - - setChannel(v as OtpChannel)} - data={[ - { - value: "phone", - label: ( - - Phone - - ), - }, - { - value: "email", - label: ( - - Email - - ), - }, - ]} - /> -
+
- {passwordValue.length > 0 ? ( -
- {passwordRequirements.map((req) => { - const met = req.test(passwordValue); - return ( -
- - {met ? ( - - ) : ( - - )} - - - {req.label} - -
- ); - })} -
- ) : null} +
) : ( - -
- - - -
-
-

- Verify your {otpChannel === "email" ? "email" : "phone"} -

-

- We sent a 6 - digit code to{" "} - - {otpChannel === "email" - ? maskEmail(pendingData?.email ?? "") - : maskPhone(pendingData?.phone ?? "")} - - .Enter it to finish creating your account. -

-
- - {otpError ? ( - } - > - {otpError} - - ) : null} - - - - Verification code - - - - - - -
- - -
-
+ { + setStage("form"); + setOtpError(null); + }} + onResend={resendOtp} + resendIn={resendCooldown.secondsLeft} + sending={sending} + verifying={verifying} + error={otpError} + description="Enter it to finish creating your account." + submitLabel="Verify & create account" + /> )}
diff --git a/apps/edr-freight-web/portal/src/services/api.ts b/apps/edr-freight-web/portal/src/services/api.ts index c5af03f65..b9937501f 100644 --- a/apps/edr-freight-web/portal/src/services/api.ts +++ b/apps/edr-freight-web/portal/src/services/api.ts @@ -77,6 +77,9 @@ import type { SetPasswordPayload, SignupPayload, SignupResponse, + ForgotPasswordRequestPayload, + ForgotPasswordVerifyPayload, + ResetTicket, } from "@/types/auth"; // --------------------------------------------------------------------------- @@ -110,6 +113,21 @@ export const api = { "setPassword", authService.setPassword, ), + requestPasswordReset: endpoint( + "auth", + "requestPasswordReset", + authService.requestPasswordReset, + ), + verifyPasswordResetOtp: endpoint( + "auth", + "verifyPasswordResetOtp", + authService.verifyPasswordResetOtp, + ), + resetPassword: endpoint( + "auth", + "resetPassword", + authService.resetPassword, + ), checkAvailability: endpoint( "auth", "checkAvailability", diff --git a/apps/edr-freight-web/portal/src/services/auth.service.ts b/apps/edr-freight-web/portal/src/services/auth.service.ts index 3f9ef4e53..3b113878d 100644 --- a/apps/edr-freight-web/portal/src/services/auth.service.ts +++ b/apps/edr-freight-web/portal/src/services/auth.service.ts @@ -3,11 +3,14 @@ import type { AuthUser, CheckAvailabilityPayload, CheckAvailabilityResponse, + ForgotPasswordRequestPayload, + ForgotPasswordVerifyPayload, GenerateVerificationCodePayload, LoginPayload, LoginResponse, OtpPayload, OtpResponse, + ResetTicket, SetPasswordPayload, SignupPayload, SignupResponse, @@ -53,6 +56,31 @@ export const authService = { return res.data.data; }, + // The three calls below drive the unauthenticated forgot-password flow. + // Responses under /api/auth are *flattened* by the API's response + // interceptor ({ success, ...payload }), so there is no `.data.data` here. + + requestPasswordReset: async (body: ForgotPasswordRequestPayload) => { + await client.post(URL_CONSTANTS.AUTH.FORGOT_PASSWORD_REQUEST, body); + }, + + verifyPasswordResetOtp: async (body: ForgotPasswordVerifyPayload) => { + const res = await client.post( + URL_CONSTANTS.AUTH.FORGOT_PASSWORD_VERIFY, + body, + ); + return { userId: res.data.userId, verificationCode: res.data.verificationCode }; + }, + + /** + * Spend the reset ticket. Distinct from `setPassword` above, which the + * authenticated post-signup flow drives through `useAuth` — this one carries + * its own userId/verificationCode and never touches the session. + */ + resetPassword: async (body: SetPasswordPayload) => { + await client.patch(URL_CONSTANTS.USERS.SET_PASSWORD, body); + }, + checkAvailability: async (params: CheckAvailabilityPayload) => { const res = await client.get( URL_CONSTANTS.USERS.CHECK_AVAILABILITY, diff --git a/apps/edr-freight-web/portal/src/types/auth.ts b/apps/edr-freight-web/portal/src/types/auth.ts index 04357a9ca..2e9a0b611 100644 --- a/apps/edr-freight-web/portal/src/types/auth.ts +++ b/apps/edr-freight-web/portal/src/types/auth.ts @@ -63,6 +63,25 @@ export interface SetPasswordPayload { verificationCode: string; } +/** The channel a password-reset code is delivered over. */ +export type ResetChannel = "email" | "phone"; + +export interface ForgotPasswordRequestPayload { + /** Email, username, or E.164 phone — whatever the user typed, normalised. */ + identifier: string; + channel: ResetChannel; +} + +export interface ForgotPasswordVerifyPayload extends ForgotPasswordRequestPayload { + otp: string; +} + +/** Single-use ticket to spend on `PATCH /api/auth/set-password`. */ +export interface ResetTicket { + userId: string; + verificationCode: string; +} + export interface GenerateVerificationCodePayload { email: string; phoneNumber: string; diff --git a/apps/edr-freight-web/portal/src/utils/identifier.ts b/apps/edr-freight-web/portal/src/utils/identifier.ts new file mode 100644 index 000000000..72677f73c --- /dev/null +++ b/apps/edr-freight-web/portal/src/utils/identifier.ts @@ -0,0 +1,22 @@ +/** Normalise Ethiopian local phone (09…/07…) to E.164; pass email through unchanged. */ +export function normaliseIdentifier(raw: string): string { + const v = raw.trim(); + const digits = v.replace(/\D/g, ""); + if (digits.length >= 9 && (v.startsWith("0") || v.startsWith("+251"))) { + const local = digits.startsWith("251") ? digits.slice(3) : digits.replace(/^0/, ""); + return `+251${local}`; + } + return v.toLowerCase(); +} + +/** Mask all but the first 7 chars of an E.164 phone for display. */ +export const maskPhone = (p: string) => + p.length > 4 ? `${p.slice(0, 7)}${"*".repeat(Math.max(0, p.length - 7))}` : p; + +/** Mask the local part of an email for display (j***e@example.com). */ +export const maskEmail = (email: string) => { + const [local, domain] = email.split("@"); + if (!local || !domain) return email; + if (local.length <= 2) return `${local[0] ?? ""}***@${domain}`; + return `${local[0]}***${local[local.length - 1]}@${domain}`; +}; diff --git a/apps/edr-freight-web/portal/src/utils/passwordSchema.ts b/apps/edr-freight-web/portal/src/utils/passwordSchema.ts new file mode 100644 index 000000000..d21d3f83e --- /dev/null +++ b/apps/edr-freight-web/portal/src/utils/passwordSchema.ts @@ -0,0 +1,36 @@ +import { z } from "zod"; + +/** Live checklist shown under the password field. Mirrors {@link passwordField}. */ +export const passwordRequirements = [ + { label: "At least 8 characters", test: (v: string) => v.length >= 8 }, + { label: "One uppercase letter", test: (v: string) => /[A-Z]/.test(v) }, + { label: "One lowercase letter", test: (v: string) => /[a-z]/.test(v) }, + { label: "One number", test: (v: string) => /\d/.test(v) }, + { label: "One special character", test: (v: string) => /[^A-Za-z0-9]/.test(v) }, +] as const; + +/** + * Must stay in step with IAM's `@IsStrongPassword()` on `InitialResetPasswordDto` + * — a password this accepts but the API rejects surfaces as an opaque 400. + */ +export const passwordField = z + .string() + .min(8, "Password must be at least 8 characters") + .regex(/[A-Z]/, "Password must include an uppercase letter") + .regex(/[a-z]/, "Password must include a lowercase letter") + .regex(/\d/, "Password must include a number") + .regex(/[^A-Za-z0-9]/, "Password must include a special character"); + +export const confirmPasswordField = z.string().min(1, "Please confirm your password"); + +export const samePassword = (data: { password: string; confirmPassword: string }) => + data.password === data.confirmPassword; + +export const PASSWORD_MISMATCH = { + message: "Passwords do not match", + path: ["confirmPassword"], +} as const; + +/** Every requirement in {@link passwordRequirements} is satisfied. */ +export const meetsAllRequirements = (value: string) => + passwordRequirements.every((r) => r.test(value)); From 3f7734fe1607d9a03986ffe7d573b6fe670bf206 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Thu, 9 Jul 2026 08:55:06 +0000 Subject: [PATCH 10/25] fixes --- .../src/pages/accounts/SetPasswordPage.tsx | 34 +++++++++++++++---- .../portal/src/pages/accounts/SignupPage.tsx | 6 ++-- .../portal/src/utils/passwordSchema.ts | 20 ++++++----- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/apps/edr-freight-web/portal/src/pages/accounts/SetPasswordPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/SetPasswordPage.tsx index ee8dd1922..04625f2cd 100644 --- a/apps/edr-freight-web/portal/src/pages/accounts/SetPasswordPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/SetPasswordPage.tsx @@ -1,4 +1,13 @@ -import { Alert, Box, Button, Group, PasswordInput, Stack, Text, ThemeIcon } from "@mantine/core"; +import { + Alert, + Box, + Button, + Group, + PasswordInput, + Stack, + Text, + ThemeIcon, +} from "@mantine/core"; import { zodResolver } from "@hookform/resolvers/zod"; import { ArrowRight, Check, LockKeyhole, X } from "lucide-react"; import { useMemo, useState } from "react"; @@ -9,7 +18,6 @@ import { z } from "zod"; import useAuth from "@/hooks/useAuth"; import AuthLayout from "@/components/auth/AuthLayout"; import { - PASSWORD_MISMATCH, confirmPasswordField, passwordField, passwordRequirements, @@ -21,7 +29,10 @@ const passwordSchema = z password: passwordField, confirmPassword: confirmPasswordField, }) - .refine(samePassword, PASSWORD_MISMATCH); + .refine(samePassword, { + message: "Passwords do not match", + path: ["confirmPassword"], + }); type FormData = z.infer; @@ -44,7 +55,8 @@ export default function SetPasswordPage() { const password = watch("password"); const requirements = useMemo( - () => passwordRequirements.map((r) => ({ ...r, met: r.test(password || "") })), + () => + passwordRequirements.map((r) => ({ ...r, met: r.test(password || "") })), [password], ); @@ -83,11 +95,21 @@ export default function SetPasswordPage() { "Secure freight operations", "Advanced authentication system", ], - stats: { label: "Security Protection", value: "256-bit", footer: "Encrypted", progress: "w-[98%]" }, + stats: { + label: "Security Protection", + value: "256-bit", + footer: "Encrypted", + progress: "w-[98%]", + }, }} > - + diff --git a/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx index fe2fb09cd..72dafe9f7 100644 --- a/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx @@ -27,7 +27,6 @@ import PasswordChecklist from "@/components/auth/PasswordChecklist"; import { ControlledPhoneField, isValidPhone } from "@/components/PhoneField"; import { api } from "@/services/api"; import { - PASSWORD_MISMATCH, confirmPasswordField, passwordField, samePassword, @@ -53,7 +52,10 @@ const userSchema = z password: passwordField, confirmPassword: confirmPasswordField, }) - .refine(samePassword, PASSWORD_MISMATCH); + .refine(samePassword, { + message: "Passwords do not match", + path: ["confirmPassword"], + }); type FormData = z.infer; diff --git a/apps/edr-freight-web/portal/src/utils/passwordSchema.ts b/apps/edr-freight-web/portal/src/utils/passwordSchema.ts index d21d3f83e..207d9dc2b 100644 --- a/apps/edr-freight-web/portal/src/utils/passwordSchema.ts +++ b/apps/edr-freight-web/portal/src/utils/passwordSchema.ts @@ -6,7 +6,10 @@ export const passwordRequirements = [ { label: "One uppercase letter", test: (v: string) => /[A-Z]/.test(v) }, { label: "One lowercase letter", test: (v: string) => /[a-z]/.test(v) }, { label: "One number", test: (v: string) => /\d/.test(v) }, - { label: "One special character", test: (v: string) => /[^A-Za-z0-9]/.test(v) }, + { + label: "One special character", + test: (v: string) => /[^A-Za-z0-9]/.test(v), + }, ] as const; /** @@ -21,15 +24,14 @@ export const passwordField = z .regex(/\d/, "Password must include a number") .regex(/[^A-Za-z0-9]/, "Password must include a special character"); -export const confirmPasswordField = z.string().min(1, "Please confirm your password"); +export const confirmPasswordField = z + .string() + .min(1, "Please confirm your password"); -export const samePassword = (data: { password: string; confirmPassword: string }) => - data.password === data.confirmPassword; - -export const PASSWORD_MISMATCH = { - message: "Passwords do not match", - path: ["confirmPassword"], -} as const; +export const samePassword = (data: { + password: string; + confirmPassword: string; +}) => data.password === data.confirmPassword; /** Every requirement in {@link passwordRequirements} is satisfied. */ export const meetsAllRequirements = (value: string) => From fa5fde6b6bcc96b17eab320b69a08ba640a40d29 Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Thu, 9 Jul 2026 08:59:58 +0000 Subject: [PATCH 11/25] Fix slow GRN button and sms timeout --- .../strategies/notification.sms.strategy.ts | 5 ++ .../warehouses/warehouse-inventory.service.ts | 47 +++++++++++++++---- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/apps/edr-freight-api/src/modules/notifications/strategies/notification.sms.strategy.ts b/apps/edr-freight-api/src/modules/notifications/strategies/notification.sms.strategy.ts index d8404e84c..cddc67b2d 100644 --- a/apps/edr-freight-api/src/modules/notifications/strategies/notification.sms.strategy.ts +++ b/apps/edr-freight-api/src/modules/notifications/strategies/notification.sms.strategy.ts @@ -22,6 +22,10 @@ export class SmsNotificationStrategy implements NotificationStrategy { this.logger.debug(`Sending SMS to ${recipient} via ${url}`); + // axios defaults to no timeout — a hanging gateway would block the caller + // (and any transaction it sits in) indefinitely. Always bound the wait. + const timeout = Number(this.configService.get("SMS_TIMEOUT_MS") ?? 8000); + try { const response = await axios.post( url, @@ -34,6 +38,7 @@ export class SmsNotificationStrategy implements NotificationStrategy { callbackUrl: "", }, { + timeout, headers: { accept: "*/*", "Content-Type": "application/json", diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts index 9b2f53a99..06e84d455 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts @@ -860,6 +860,25 @@ export class WarehouseInventoryService { /** Bulk-receive eligible PAID bookings into a location. Skips duplicates / wrong direction. */ async bulkReceive(dto: BulkReceiveDto): Promise { const result: BulkReceiveResult = { receivedCount: 0, skippedCount: 0, results: [] }; + /** Sent after the transaction commits so the gateway never blocks the receive. */ + const pendingNotifications: Array<{ + owner: { + phone?: string | null; + ownerName?: string | null; + bookingReference?: string | null; + grnNumber: string; + direction?: string | null; + warehouseId?: string | null; + }; + booking: { + companyId?: string | null; + reference?: string | null; + hasFirstMile?: boolean; + hasLastMile?: boolean; + customerTruckAssignedAt?: string | null; + }; + bookingId: string; + }> = []; await this.dataSource.transaction(async (manager) => { await this.validateLocation(manager, { @@ -1032,21 +1051,33 @@ export class WarehouseInventoryService { manager, ); - await this.notifyOwnerInventoryReceived({ - phone: truckEntrance?.customerPhone ?? booking.customerPhone, - ownerName: truckEntrance?.ownerName ?? booking.customer, - bookingReference: truckEntrance?.edrDigitalBookingId ?? booking.reference, - grnNumber, - direction: dto.direction, - warehouseId: dto.warehouseId, + // Queued, not sent here: an SMS/email round-trip inside the transaction + // holds capacity/location locks open for the whole gateway latency. + pendingNotifications.push({ + owner: { + phone: truckEntrance?.customerPhone ?? booking.customerPhone, + ownerName: truckEntrance?.ownerName ?? booking.customer, + bookingReference: truckEntrance?.edrDigitalBookingId ?? booking.reference, + grnNumber, + direction: dto.direction, + warehouseId: dto.warehouseId, + }, + booking, + bookingId, }); result.receivedCount += 1; result.results.push({ bookingId, status: 'RECEIVED', inventoryId: saved.id, grnNumber }); - void this.notifyTruckAssignmentNeeded(booking, bookingId); } }); + // Fan out after commit, un-awaited: the receive response must not wait on the + // SMS gateway. Both notifiers swallow their own errors. + for (const pending of pendingNotifications) { + void this.notifyOwnerInventoryReceived(pending.owner); + void this.notifyTruckAssignmentNeeded(pending.booking, pending.bookingId); + } + return result; } From f7ceca944ad50de090983b7a3ca307de5dd2d55b Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Thu, 9 Jul 2026 09:21:15 +0000 Subject: [PATCH 12/25] Freight OperationsQA Test Plan --- docs/qa/edr-freight-qa-test-plan.pdf | 6680 ++++++++++++++++++++++++++ 1 file changed, 6680 insertions(+) create mode 100644 docs/qa/edr-freight-qa-test-plan.pdf diff --git a/docs/qa/edr-freight-qa-test-plan.pdf b/docs/qa/edr-freight-qa-test-plan.pdf new file mode 100644 index 000000000..56c5d520d --- /dev/null +++ b/docs/qa/edr-freight-qa-test-plan.pdf @@ -0,0 +1,6680 @@ +%PDF-1.3 +%߬ +3 0 obj +<> +endobj +4 0 obj +<< +/Length 9916 +>> +stream +0.200025 w +0 G +BT +/F5 8 Tf +9.1999999999999993 TL +0.043 0.333 0.388 rg +42. 793.8899999999999864 Td +(ETHIO-DJIBOUTI RAILWAY / FREIGHT PLATFORM) Tj +ET +BT +/F9 30 Tf +34.5 TL +0.063 0.102 0.11 rg +42. 767.8899999999999864 Td +(Freight Operations) Tj +ET +BT +/F9 30 Tf +34.5 TL +0.063 0.102 0.11 rg +42. 737.8899999999999864 Td +(QA Test Plan) Tj +ET +0.06 0.1 0.11 RG +1.3999999999999999 w +42. 717.8899999999999864 m +553.2799999999999727 717.8899999999999864 l +S +BT +/F1 10 Tf +11.5 TL +0.275 0.345 0.357 rg +42. 697.8899999999999864 Td +(End-to-end test flows from booking through warehouse, rail, and delivery - import and export,) Tj +T* (with and without first/last mile, self-haul and EDR haulage. Every status, guard, and endpoint) Tj +T* (below is taken from the code, not assumed.) Tj +ET +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.427 0.498 0.51 rg +42. 642.8899999999999864 Td +(BRANCH Truckdetantion) Tj +ET +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.427 0.498 0.51 rg +212. 642.8899999999999864 Td +(SCOPE Warehouse . Fees . Allocation . First mile . Last mile) Tj +ET +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.427 0.498 0.51 rg +42. 630.8899999999999864 Td +(DEPTH Tester steps + technical refs) Tj +ET +BT +/F5 9 Tf +10.3499999999999996 TL +0.043 0.333 0.388 rg +42. 596.8899999999999864 Td +(00) Tj +ET +BT +/F9 17 Tf +19.5499999999999972 TL +0.063 0.102 0.11 rg +72. 595.8899999999999864 Td +(Test data setup) Tj +ET +0.06 0.1 0.11 RG +0.9 w +42. 575.8899999999999864 m +553.2799999999999727 575.8899999999999864 l +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +42. 561.8899999999999864 Td +(Nothing below passes without this. Set it up once per environment and confirm each line before opening a single flow.) Tj +ET +0.43 0.5 0.51 RG +0.6 w +43. 547.3899999999999864 8. -8. re +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +60. 539.8899999999999864 Td +(Warehouse tree: at least one ACTIVE warehouse with a yard and a zone. Capacities are in TONNES, not kg.) Tj +ET +0.43 0.5 0.51 RG +0.6 w +43. 530.3899999999999864 8. -8. re +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +60. 522.8899999999999864 Td +(Company has a linked portal user. Critical - in-app notifications resolve recipients from the company's portal users. With) Tj +T* (none linked, notify\(\) logs "0 recipients - skipped" and stores nothing. SMS/email still fire.) Tj +ET +0.43 0.5 0.51 RG +0.6 w +43. 501.3899999999999864 8. -8. re +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +60. 493.8899999999999864 Td +(Customer saved signature. Required for Approve delivery; without it the API returns "Please save your signature before) Tj +T* (approving delivery".) Tj +ET +0.43 0.5 0.51 RG +0.6 w +43. 472.3899999999999864 8. -8. re +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +60. 464.8899999999999864 Td +(Allocation rules covering the freight type and trade direction under test \(see 08\), or accept the capacity-balanced fallback.) Tj +ET +0.43 0.5 0.51 RG +0.6 w +43. 455.3899999999999864 8. -8. re +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +60. 447.8899999999999864 Td +(Fee rules: at least one each of STORAGE_FEE, DEMURRAGE_FEE, DOUBLE_HANDLING_FEE,) Tj +T* (TRUCK_DETENTION_FEE \(see 07\).) Tj +ET +0.43 0.5 0.51 RG +0.6 w +43. 426.3899999999999864 8. -8. re +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +60. 418.8899999999999864 Td +(Drivers and vehicles registered; a train schedule with wagons for the route under test.) Tj +ET +0.43 0.5 0.51 RG +0.6 w +43. 409.3899999999999864 8. -8. re +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +60. 401.8899999999999864 Td +(Booking reaches PAID. Receive-to-warehouse skips any booking that is not PAID.) Tj +ET +0.43 0.5 0.51 RG +0.6 w +43. 392.3899999999999864 8. -8. re +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +60. 384.8899999999999864 Td +(Container numbers are ISO 6346 - 4 letters + 7 digits, uppercase \(ABCU1234567\). Enforced at booking input and every) Tj +T* (reference point.) Tj +ET +0.89 0.94 0.95 rg +42. 351.8899999999999864 511.2799999999999727 -51. re +f +0.04 0.33 0.39 rg +42. 351.8899999999999864 3. -51. re +f +BT +/F1 8.6 Tf +9.8899999999999988 TL +0.275 0.345 0.357 rg +56. 338.8899999999999864 Td +(Direction is derived, not declared. Receive-to-warehouse computes trade direction from the ORIGIN and DESTINATION YARD) Tj +T* (COUNTRIES, not the booking's stored trade_direction. A booking whose route says IMPORT will be skipped from an EXPORT) Tj +T* (receive with "Booking route is IMPORT, not EXPORT". Set up yards accordingly.) Tj +ET +BT +/F5 9 Tf +10.3499999999999996 TL +0.043 0.333 0.388 rg +42. 278.8899999999999864 Td +(01) Tj +ET +BT +/F9 17 Tf +19.5499999999999972 TL +0.063 0.102 0.11 rg +72. 277.8899999999999864 Td +(Lifecycle reference) Tj +ET +0.06 0.1 0.11 RG +0.9 w +42. 257.8899999999999864 m +553.2799999999999727 257.8899999999999864 l +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +42. 243.8899999999999864 Td +(The three state machines a tester needs to read a failure. Anything not listed as an allowed transition is rejected by) Tj +T* (assertTransition.) Tj +ET +BT +/F2 8 Tf +9.1999999999999993 TL +0.063 0.102 0.11 rg +42. 203.8899999999999864 Td +(WAREHOUSE INVENTORY TRANSITIONS) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +42. 191.8899999999999864 105. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +47. 180.9399999999999409 Td +(From) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +147. 191.8899999999999864 145. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +152. 180.9399999999999409 Td +(Allowed next) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +292. 191.8899999999999864 261.2799999999999159 -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +297. 180.9399999999999409 Td +(Notes) Tj +ET +0.06 0.1 0.11 RG +0.9 w +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 173.8400000000000318 105. -18.7399999999999984 re +B +BT +/F5 7 Tf +8.0499999999999989 TL +0.063 0.102 0.11 rg +47. 162.8899999999999864 Td +(UNLOADED) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +147. 173.8400000000000318 145. -18.7399999999999984 re +B +BT +/F5 7 Tf +8.0499999999999989 TL +0.275 0.345 0.357 rg +152. 162.8899999999999864 Td +(STORED, READY_FOR_PICKUP) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +292. 173.8400000000000318 261.2799999999999159 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +297. 162.3799999999999955 Td +(Import landing state after train unload) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 155.1000000000000227 105. -18.7399999999999984 re +B +BT +/F5 7 Tf +8.0499999999999989 TL +0.063 0.102 0.11 rg +47. 144.1499999999999773 Td +(RECEIVED) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +147. 155.1000000000000227 145. -18.7399999999999984 re +B +BT +/F5 7 Tf +8.0499999999999989 TL +0.275 0.345 0.357 rg +152. 144.1499999999999773 Td +(STORED, READY_FOR_PICKUP) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +292. 155.1000000000000227 261.2799999999999159 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +297. 143.6399999999999864 Td +(Export landing state after truck receive) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 136.3600000000000136 105. -27.4799999999999969 re +B +BT +/F5 7 Tf +8.0499999999999989 TL +0.063 0.102 0.11 rg +47. 125.4099999999999682 Td +(STORED) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +147. 136.3600000000000136 145. -27.4799999999999969 re +B +BT +/F5 7 Tf +8.0499999999999989 TL +0.275 0.345 0.357 rg +152. 125.4099999999999682 Td +(RESERVED, READY_FOR_LOADING) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +292. 136.3600000000000136 261.2799999999999159 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +297. 124.8999999999999773 Td +(Reserve is retired from the UI; STORED -> READY_FOR_LOADING is the) Tj +T* (live path) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 108.8799999999999955 105. -18.7399999999999984 re +B +BT +/F5 7 Tf +8.0499999999999989 TL +0.063 0.102 0.11 rg +47. 97.92999999999995 Td +(READY_FOR_LOADING) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +147. 108.8799999999999955 145. -18.7399999999999984 re +B +BT +/F5 7 Tf +8.0499999999999989 TL +0.275 0.345 0.357 rg +152. 97.92999999999995 Td +(LOADED) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +292. 108.8799999999999955 261.2799999999999159 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +297. 97.4199999999999591 Td +(Onto a wagon) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 90.1399999999999864 105. -18.7399999999999984 re +B +BT +/F5 7 Tf +8.0499999999999989 TL +0.063 0.102 0.11 rg +47. 79.1899999999999409 Td +(LOADED) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +147. 90.1399999999999864 145. -18.7399999999999984 re +B +BT +/F5 7 Tf +8.0499999999999989 TL +0.275 0.345 0.357 rg +152. 79.1899999999999409 Td +(DISPATCHED) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +292. 90.1399999999999864 261.2799999999999159 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +297. 78.67999999999995 Td +() Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 71.3999999999999773 105. -18.7399999999999984 re +B +BT +/F5 7 Tf +8.0499999999999989 TL +0.063 0.102 0.11 rg +47. 60.4499999999999318 Td +(DISPATCHED) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +147. 71.3999999999999773 145. -18.7399999999999984 re +B +BT +/F5 7 Tf +8.0499999999999989 TL +0.275 0.345 0.357 rg +152. 60.4499999999999318 Td +(UNLOADED_AT_DJIBOUTI_PORT) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +292. 71.3999999999999773 261.2799999999999159 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +297. 59.9399999999999409 Td +(Export only, at Djibouti) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +0.78 G +0. w +0.8 0.85 0.85 RG +0.5 w +42. 36. m +553.2799999999999727 36. l +S +BT +/F9 8 Tf +9.1999999999999993 TL +0.427 0.498 0.51 rg +42. 22. Td +(EDR Freight - QA Test Plan) Tj +ET +BT +/F5 7.5 Tf +8.625 TL +0.427 0.498 0.51 rg +530.7799999999999727 22. Td +(1 / 7) Tj +ET +endstream +endobj +5 0 obj +<> +endobj +6 0 obj +<< +/Length 16091 +>> +stream +0. w +0.78 G +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +42. 801.8899999999999864 105. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +47. 790.9399999999999409 Td +(From) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +147. 801.8899999999999864 145. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +152. 790.9399999999999409 Td +(Allowed next) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +292. 801.8899999999999864 261.2799999999999159 -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +297. 790.9399999999999409 Td +(Notes) Tj +ET +0.06 0.1 0.11 RG +0.9 w +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 783.8400000000000318 105. -18.7399999999999984 re +B +BT +/F5 7 Tf +8.0499999999999989 TL +0.063 0.102 0.11 rg +47. 772.8899999999999864 Td +(READY_FOR_PICKUP) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +147. 783.8400000000000318 145. -18.7399999999999984 re +B +BT +/F5 7 Tf +8.0499999999999989 TL +0.275 0.345 0.357 rg +152. 772.8899999999999864 Td +(DELIVERED, STORED, DISPATCHED) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +292. 783.8400000000000318 261.2799999999999159 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +297. 772.3799999999999955 Td +(Import; may be put back into storage) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.78 G +0. w +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +BT +/F2 8 Tf +9.1999999999999993 TL +0.063 0.102 0.11 rg +42. 743.1000000000000227 Td +(CONTAINER ITEM STAGES) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.5 w +42. 733.1000000000000227 511.2799999999999727 -21. re +B +BT +/F5 8 Tf +9.1999999999999993 TL +0.275 0.345 0.357 rg +52. 721.1000000000000227 Td +(PENDING > RECEIVED > GRN > ASSIGNED > LOADED > LEFT > DELIVERED) Tj +ET +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +42. 700.1000000000000227 Td +(ASSIGNED means the customer picked which containers ride which truck - planning only. LOADED requires the operator to) Tj +T* (actually load them, and only after the truck has arrived \(loaded_at is stamped then\). Assignment alone must never show) Tj +T* (LOADED.) Tj +ET +BT +/F2 8 Tf +9.1999999999999993 TL +0.063 0.102 0.11 rg +42. 650.1000000000000227 Td +(FIRST MILE & LAST MILE) Tj +ET +0.8 0.85 0.85 RG +0.5 w +0.8 0.85 0.85 RG +0.5 w +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +42. 638.1000000000000227 56. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +47. 627.1499999999999773 Td +(Leg) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +98. 638.1000000000000227 210. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +103. 627.1499999999999773 Td +(Statuses, in order) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +308. 638.1000000000000227 245.2799999999999727 -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +313. 627.1499999999999773 Td +(Gate it controls) Tj +ET +0.8 0.85 0.85 RG +0.5 w +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 620.0499999999999545 56. -26.0999999999999979 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 608.5900000000000318 Td +(First mile) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +98. 620.0499999999999545 210. -26.0999999999999979 re +B +BT +/F5 7 Tf +8.0499999999999989 TL +0.275 0.345 0.357 rg +103. 609.1000000000000227 Td +(PAYMENT_PENDING -> READY_TO_TRANSIT ->) Tj +T* (IN_TRANSIT -> RECEIVED_TO_PORT) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +308. 620.0499999999999545 245.2799999999999727 -26.0999999999999979 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +313. 608.5900000000000318 Td +(Export receive is blocked until RECEIVED_TO_PORT) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 593.9500000000000455 56. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 582.4900000000000091 Td +(Last mile) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +98. 593.9500000000000455 210. -27.4799999999999969 re +B +BT +/F5 7 Tf +8.0499999999999989 TL +0.275 0.345 0.357 rg +103. 583. Td +(PAYMENT_PENDING -> READY_TO_TRANSIT ->) Tj +T* (IN_TRANSIT -> DELIVERED) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +308. 593.9500000000000455 245.2799999999999727 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +313. 582.4900000000000091 Td +(Truck-detention window: arrivedAt \(reached destination\) -> deliveredAt) Tj +T* (\(vehicle returned\)) Tj +ET +0.8 0.85 0.85 RG +0.5 w +0.78 G +0. w +0.8 0.85 0.85 RG +0.5 w +0.8 0.85 0.85 RG +0.5 w +BT +/F5 9 Tf +10.3499999999999996 TL +0.043 0.333 0.388 rg +42. 540.4700000000000273 Td +(02) Tj +ET +BT +/F9 17 Tf +19.5499999999999972 TL +0.063 0.102 0.11 rg +72. 539.4700000000000273 Td +(Export - without first mile) Tj +ET +0.06 0.1 0.11 RG +0.9 w +42. 519.4700000000000273 m +553.2799999999999727 519.4700000000000273 l +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +42. 505.4700000000000273 Td +(Customer brings the cargo to the facility themselves. The happy path from a paid booking to cargo unloaded at Djibouti port with) Tj +T* (an interchange document.) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +42. 471.4700000000000273 34. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +47. 460.5200000000000387 Td +(Step) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +76. 471.4700000000000273 150. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +81. 460.5200000000000387 Td +(Tester action) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +226. 471.4700000000000273 227.2799999999999159 -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +231. 460.5200000000000387 Td +(Expected result) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +453.2799999999999159 471.4700000000000273 100. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +458.2799999999999159 460.5200000000000387 Td +(Technical ref) Tj +ET +0.06 0.1 0.11 RG +0.9 w +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 453.4200000000000159 34. -27.4799999999999969 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 441.9600000000000364 Td +(E1.1) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 453.4200000000000159 150. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 441.9600000000000364 Td +(Create an export booking \(route origin ET) Tj +T* (-> destination DJ\), pay it.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +226. 453.4200000000000159 227.2799999999999159 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +231. 441.9600000000000364 Td +(Booking reaches PAID.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +453.2799999999999159 453.4200000000000159 100. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +458.2799999999999159 441.9600000000000364 Td +(Container numbers must) Tj +T* (be ISO 6346) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 425.9399999999999977 34. -44.9599999999999937 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 414.4800000000000182 Td +(E1.2) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 425.9399999999999977 150. -44.9599999999999937 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 414.4800000000000182 Td +(Export Operations > Receive for Loading.) Tj +T* (Select the booking, capture the truck) Tj +T* (entrance \(plate, driver, weights\), pick) Tj +T* (warehouse/yard/zone.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +226. 425.9399999999999977 227.2799999999999159 -44.9599999999999937 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +231. 414.4800000000000182 Td +(Inventory created; GRN issued as) Tj +T* (GRN-EXPORT-YYYYMMDD-XXXXXXXX. Customer gets a) Tj +T* (receive SMS.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +453.2799999999999159 425.9399999999999977 100. -44.9599999999999937 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +458.2799999999999159 414.4800000000000182 Td +(POST /warehouse-invento) Tj +T* (ry/receive-bulk) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 380.9800000000000182 34. -36.2199999999999989 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 369.5200000000000387 Td +(E1.3) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 380.9800000000000182 150. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 369.5200000000000387 Td +(Store the item - either let it auto-allocate,) Tj +T* (or pick warehouse/yard/zone in the Store) Tj +T* (modal.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +226. 380.9800000000000182 227.2799999999999159 -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +231. 369.5200000000000387 Td +(Status STORED; note records "allocation rule",) Tj +T* ("capacity-balanced", or "operator-selected".) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +453.2799999999999159 380.9800000000000182 100. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +458.2799999999999159 369.5200000000000387 Td +(Capacity decremented in) Tj +T* (tonnes) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 344.7599999999999909 34. -27.4799999999999969 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 333.3000000000000114 Td +(E1.4) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 344.7599999999999909 150. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 333.3000000000000114 Td +(Inspect: mark selected items as) Tj +T* (inspected, outcome PASSED.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +226. 344.7599999999999909 227.2799999999999159 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +231. 333.3000000000000114 Td +(Export items advance straight to READY_FOR_LOADING.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +453.2799999999999159 344.7599999999999909 100. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +458.2799999999999159 333.3000000000000114 Td +(Reserve step is retired) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 317.2799999999999727 34. -27.4799999999999969 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 305.8199999999999363 Td +(E1.5) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 317.2799999999999727 150. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 305.8199999999999363 Td +(Ready To Load tab > load onto the) Tj +T* (allocated wagon.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +226. 317.2799999999999727 227.2799999999999159 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +231. 305.8199999999999363 Td +(Status LOADED; a warehouse loading record exists.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +453.2799999999999159 317.2799999999999727 100. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +458.2799999999999159 305.8199999999999363 Td +(Requires an allocated) Tj +T* (wagon) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 289.7999999999999545 34. -27.4799999999999969 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 278.3399999999999181 Td +(E1.6) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 289.7999999999999545 150. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 278.3399999999999181 Td +(Download the export marshalling / load) Tj +T* (list PDF.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +226. 289.7999999999999545 227.2799999999999159 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +231. 278.3399999999999181 Td +(PDF lists the train's wagons, bookings, containers.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +453.2799999999999159 289.7999999999999545 100. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +458.2799999999999159 278.3399999999999181 Td +(train-scheduling controller) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 262.3199999999999363 34. -27.4799999999999969 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 250.8599999999999 Td +(E1.7) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 262.3199999999999363 150. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 250.8599999999999 Td +(Dispatch Queue > dispatch.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +226. 262.3199999999999363 227.2799999999999159 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +231. 250.8599999999999 Td +(Status DISPATCHED. Customer receives "Shipment dispatched") Tj +T* (naming origin -> destination.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +453.2799999999999159 262.3199999999999363 100. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +458.2799999999999159 250.8599999999999 Td +(Per booking on the) Tj +T* (schedule) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 234.8399999999999181 34. -27.4799999999999969 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 223.3799999999998818 Td +(E1.8) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 234.8399999999999181 150. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 223.3799999999998818 Td +(Move the schedule to arrived at the) Tj +T* (Djibouti-side port.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +226. 234.8399999999999181 227.2799999999999159 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +231. 223.3799999999998818 Td +(Train appears in the Djibouti unloading queue. Customer) Tj +T* (receives "Shipment arrived".) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +453.2799999999999159 234.8399999999999181 100. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +458.2799999999999159 223.3799999999998818 Td +(status ARRIVED /) Tj +T* (ARRIVED_AT_DJIBOUTI) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 207.3599999999999 34. -27.4799999999999969 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 195.8999999999998636 Td +(E1.9) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 207.3599999999999 150. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 195.8999999999998636 Td +(Grant the gate pass for the train, then) Tj +T* (Unload at Djibouti.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +226. 207.3599999999999 227.2799999999999159 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +231. 195.8999999999998636 Td +(Items become UNLOADED_AT_DJIBOUTI_PORT and an) Tj +T* (interchange document is generated.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +453.2799999999999159 207.3599999999999 100. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +458.2799999999999159 195.8999999999998636 Td +(Unload checks) Tj +T* (gatepass_granted_at) Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 179.8799999999998818 34. -27.4799999999999969 re +B +BT +/F6 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 168.4199999999998454 Td +(E1.G) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 179.8799999999998818 150. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 168.4199999999998454 Td +(Try to unload at Djibouti BEFORE) Tj +T* (granting the gate pass.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +226. 179.8799999999998818 227.2799999999999159 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +231. 168.4199999999998454 Td +(BLOCKED. Items skipped with a gate-pass reason; no) Tj +T* (interchange document.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +453.2799999999999159 179.8799999999998818 100. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +458.2799999999999159 168.4199999999998454 Td +(See section 06) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.78 G +0. w +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +0.8 0.85 0.85 RG +0.5 w +42. 36. m +553.2799999999999727 36. l +S +BT +/F9 8 Tf +9.1999999999999993 TL +0.427 0.498 0.51 rg +42. 22. Td +(EDR Freight - QA Test Plan) Tj +ET +BT +/F5 7.5 Tf +8.625 TL +0.427 0.498 0.51 rg +530.7799999999999727 22. Td +(2 / 7) Tj +ET +endstream +endobj +7 0 obj +<> +endobj +8 0 obj +<< +/Length 18643 +>> +stream +0.9 w +0.06 0.1 0.11 RG +BT +/F5 9 Tf +10.3499999999999996 TL +0.043 0.333 0.388 rg +42. 783.8899999999999864 Td +(03) Tj +ET +BT +/F9 17 Tf +19.5499999999999972 TL +0.063 0.102 0.11 rg +72. 782.8899999999999864 Td +(Export - with first mile) Tj +ET +0.06 0.1 0.11 RG +0.9 w +42. 762.8899999999999864 m +553.2799999999999727 762.8899999999999864 l +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +42. 748.8899999999999864 Td +(EDR collects the cargo from the customer's premises. Identical to section 02 from the store step onward; the difference is entirely) Tj +T* (in the gate before receive.) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +42. 714.8899999999999864 40. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +47. 703.9400000000000546 Td +(Step) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +82. 714.8899999999999864 150. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +87. 703.9400000000000546 Td +(Tester action) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +232. 714.8899999999999864 221.2800000000000296 -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +237. 703.9400000000000546 Td +(Expected result) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +453.2800000000000296 714.8899999999999864 100. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +458.2800000000000296 703.9400000000000546 Td +(Technical ref) Tj +ET +0.06 0.1 0.11 RG +0.9 w +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 696.8399999999999181 40. -36.2199999999999989 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 685.3799999999999955 Td +(E2.1) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +82. 696.8399999999999181 150. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +87. 685.3799999999999955 Td +(Create the export booking with a first-mile) Tj +T* (pickup address \(or a service type that) Tj +T* (includes first mile\).) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +232. 696.8399999999999181 221.2800000000000296 -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +237. 685.3799999999999955 Td +(Booking is flagged hasFirstMile.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +453.2800000000000296 696.8399999999999181 100. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +458.2800000000000296 685.3799999999999955 Td +(Derived from address OR ) Tj +T* (service_types.includes_firs) Tj +T* (t_mile) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 660.6200000000000045 40. -27.4799999999999969 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 649.1599999999999682 Td +(E2.2) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +82. 660.6200000000000045 150. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +87. 649.1599999999999682 Td +(Create the first-mile request; assign a) Tj +T* (vehicle and driver.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +232. 660.6200000000000045 221.2800000000000296 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +237. 649.1599999999999682 Td +(Driver receives an SMS naming the vehicle, booking, pickup) Tj +T* (and destination.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +453.2800000000000296 660.6200000000000045 100. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +458.2800000000000296 649.1599999999999682 Td +(First Mile page) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 633.1399999999999864 40. -27.4799999999999969 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 621.67999999999995 Td +(E2.3) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +82. 633.1399999999999864 150. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +87. 621.67999999999995 Td +(Walk the leg: READY_TO_TRANSIT ->) Tj +T* (IN_TRANSIT -> RECEIVED_TO_PORT.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +232. 633.1399999999999864 221.2800000000000296 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +237. 621.67999999999995 Td +(Each transition persists.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +453.2800000000000296 633.1399999999999864 100. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +458.2800000000000296 621.67999999999995 Td +() Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 605.6599999999999682 40. -27.4799999999999969 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 594.2000000000000455 Td +(E2.4) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +82. 605.6599999999999682 150. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +87. 594.2000000000000455 Td +(Now run Receive for Loading.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +232. 605.6599999999999682 221.2800000000000296 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +237. 594.2000000000000455 Td +(Booking is received; truck entrance pre-filled from the first-mile) Tj +T* (vehicle and driver.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +453.2800000000000296 605.6599999999999682 100. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +458.2800000000000296 594.2000000000000455 Td +(Then continue at E1.3) Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 578.1800000000000637 40. -27.4799999999999969 re +B +BT +/F6 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 566.7200000000000273 Td +(E2.G1) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +82. 578.1800000000000637 150. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +87. 566.7200000000000273 Td +(Attempt receive with NO first-mile request) Tj +T* (created.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +232. 578.1800000000000637 221.2800000000000296 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +237. 566.7200000000000273 Td +(SKIPPED: "First-mile request not created".) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +453.2800000000000296 578.1800000000000637 100. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +458.2800000000000296 566.7200000000000273 Td +() Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 550.7000000000000455 40. -36.2199999999999989 re +B +BT +/F6 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 539.2400000000000091 Td +(E2.G2) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +82. 550.7000000000000455 150. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +87. 539.2400000000000091 Td +(Attempt receive while first-mile status is) Tj +T* (IN_TRANSIT.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +232. 550.7000000000000455 221.2800000000000296 -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +237. 539.2400000000000091 Td +(SKIPPED: "First-mile truck has not arrived".) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +453.2800000000000296 550.7000000000000455 100. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +458.2800000000000296 539.2400000000000091 Td +(Only) Tj +T* (RECEIVED_TO_PORT) Tj +T* (passes) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.78 G +0. w +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +BT +/F5 9 Tf +10.3499999999999996 TL +0.043 0.333 0.388 rg +42. 488.4800000000000182 Td +(04) Tj +ET +BT +/F9 17 Tf +19.5499999999999972 TL +0.063 0.102 0.11 rg +72. 487.4800000000000182 Td +(Import - self-haul \(customer collects\)) Tj +ET +0.06 0.1 0.11 RG +0.9 w +42. 467.4800000000000182 m +553.2799999999999727 467.4800000000000182 l +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +42. 453.4800000000000182 Td +(The longest flow, and the one with the most guards. The customer assigns their own trucks, signs a booking-level handover, and) Tj +T* (collects. Test this one first - it exercises truck arrival, loading, weighing, handover, exit paper, and fees.) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.5 w +42. 421.4800000000000182 511.2799999999999727 -32. re +B +BT +/F5 8 Tf +9.1999999999999993 TL +0.275 0.345 0.357 rg +52. 409.4800000000000182 Td +(train arrives > unload > inspect > ready for pickup > assign truck > truck arrival > sign) Tj +T* (handover > load > truck leaving > exit paper > deliver) Tj +ET +0.8 0.85 0.85 RG +0.5 w +0.8 0.85 0.85 RG +0.5 w +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +42. 377.4800000000000182 34. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +47. 366.5300000000000296 Td +(Step) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +76. 377.4800000000000182 145. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +81. 366.5300000000000296 Td +(Tester action) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +221. 377.4800000000000182 234.2800000000000296 -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +226. 366.5300000000000296 Td +(Expected result) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +455.2800000000000296 377.4800000000000182 98. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +460.2800000000000296 366.5300000000000296 Td +(Technical ref) Tj +ET +0.8 0.85 0.85 RG +0.5 w +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 359.4300000000000068 34. -36.2199999999999989 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 347.9700000000000273 Td +(I1.1) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 359.4300000000000068 145. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 347.9700000000000273 Td +(Import booking \(route DJ -> ET\), paid,) Tj +T* (no last-mile address and a service type) Tj +T* (that excludes last mile.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +221. 359.4300000000000068 234.2800000000000296 -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +226. 347.9700000000000273 Td +(Booking is self-haul.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +455.2800000000000296 359.4300000000000068 98. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +460.2800000000000296 347.9700000000000273 Td +(Drives) Tj +T* (usesCustomerTruck) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 323.2100000000000364 34. -44.9599999999999937 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 311.75 Td +(I1.2) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 323.2100000000000364 145. -44.9599999999999937 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 311.75 Td +(Import Operations > Arrival Queue.) Tj +T* (Open the arrived train, assign) Tj +T* (warehouse/yard/zone per booking, Auto) Tj +T* (Unload Arrived Bookings.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +221. 323.2100000000000364 234.2800000000000296 -44.9599999999999937 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +226. 311.75 Td +(Inventory created at UNLOADED. Counter shows n/total unloaded.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +455.2800000000000296 323.2100000000000364 98. -44.9599999999999937 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +460.2800000000000296 311.75 Td +(Train must be ARRIVED) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 278.25 34. -27.4799999999999969 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 266.7899999999999636 Td +(I1.3) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 278.25 145. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 266.7899999999999636 Td +(Inspect the item, outcome PASSED.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +221. 278.25 234.2800000000000296 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +226. 266.7899999999999636 Td +(Item advances to READY_FOR_PICKUP. Customer receives) Tj +T* ("Assign a truck for pickup" \(in-app + SMS + email\).) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +455.2800000000000296 278.25 98. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +460.2800000000000296 266.7899999999999636 Td +(Fires only when self-haul) Tj +T* (AND no truck assigned) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 250.7699999999999818 34. -36.2199999999999989 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 239.3099999999999454 Td +(I1.4) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 250.7699999999999818 145. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 239.3099999999999454 Td +(PORTAL: customer assigns truck\(s\),) Tj +T* (entering ISO container numbers per) Tj +T* (truck.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +221. 250.7699999999999818 234.2800000000000296 -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +226. 239.3099999999999454 Td +(Containers move to stage ASSIGNED and show their planned) Tj +T* (truck. Booking becomes TRUCK_ASSIGNED.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +455.2800000000000296 250.7699999999999818 98. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +460.2800000000000296 239.3099999999999454 Td +(20ft -> max 2/truck; 40ft ->) Tj +T* (1/truck; trucks <=) Tj +T* (containers) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 214.5499999999999545 34. -36.2199999999999989 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 203.0899999999999181 Td +(I1.5) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 214.5499999999999545 145. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 203.0899999999999181 Td +(Backoffice row menu > Truck Arrival.) Tj +T* (Select the assigned truck, record gate-in) Tj +T* (time and TARE weight \(tonnes\).) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +221. 214.5499999999999545 234.2800000000000296 -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +226. 203.0899999999999181 Td +(Truck stamped arrived. A SELF_HAUL handover is generated) Tj +T* (\(booking-level\) and the customer is notified to sign, on all three) Tj +T* (channels.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +455.2800000000000296 214.5499999999999545 98. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +460.2800000000000296 203.0899999999999181 Td +(Truck Arrival disabled until) Tj +T* (a truck is assigned) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 178.3299999999999272 34. -27.4799999999999969 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 166.8699999999998909 Td +(I1.6) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 178.3299999999999272 145. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 166.8699999999998909 Td +(PORTAL: customer opens the booking >) Tj +T* (Approve delivery.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +221. 178.3299999999999272 234.2800000000000296 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +226. 166.8699999999998909 Td +(The handover PDF opens for review; approving applies their saved) Tj +T* (signature and returns the signed PDF.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +455.2800000000000296 178.3299999999999272 98. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +460.2800000000000296 166.8699999999998909 Td +(Signs all unsigned) Tj +T* (handovers on the booking) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 150.8499999999999091 34. -36.2199999999999989 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 139.3899999999998727 Td +(I1.7) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 150.8499999999999091 145. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 139.3899999999998727 Td +(Open the container list > select the) Tj +T* (assigned containers > Load onto truck) Tj +T* (\(pick the arrived truck\).) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +221. 150.8499999999999091 234.2800000000000296 -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +226. 139.3899999999998727 Td +(Containers move to LOADED; loaded_at stamped.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +455.2800000000000296 150.8499999999999091 98. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +460.2800000000000296 139.3899999999998727 Td +(Only arrived, not-departed) Tj +T* (trucks are listed) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 114.6299999999998818 34. -36.2199999999999989 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 103.1699999999998454 Td +(I1.8) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 114.6299999999998818 145. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 103.1699999999998454 Td +(Row menu > Truck Leaving. Select the) Tj +T* (containers on the truck, record gate-out) Tj +T* (time and GROSS weight.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +221. 114.6299999999998818 234.2800000000000296 -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +226. 103.1699999999998454 Td +(Net is computed from the selected containers' cargo weight and) Tj +T* (must equal gross - tare. Release document issued.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +455.2800000000000296 114.6299999999998818 98. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +460.2800000000000296 103.1699999999998454 Td +(Weight match enforced) Tj +T* (client- AND server-side) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 78.4099999999998545 34. -18.7399999999999984 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 66.9499999999998181 Td +(I1.9) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 78.4099999999998545 145. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 66.9499999999998181 Td +(Generate the exit paper for the truck.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +221. 78.4099999999998545 234.2800000000000296 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +226. 66.9499999999998181 Td +(PDF lists the truck, driver, and its containers. Containers move to) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +455.2800000000000296 78.4099999999998545 98. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +460.2800000000000296 66.9499999999998181 Td +(Requires signed handover) Tj +ET +0.8 0.85 0.85 RG +0.5 w +0.8 0.85 0.85 RG +0.5 w +0.78 G +0. w +0.8 0.85 0.85 RG +0.5 w +42. 36. m +553.2799999999999727 36. l +S +BT +/F9 8 Tf +9.1999999999999993 TL +0.427 0.498 0.51 rg +42. 22. Td +(EDR Freight - QA Test Plan) Tj +ET +BT +/F5 7.5 Tf +8.625 TL +0.427 0.498 0.51 rg +530.7799999999999727 22. Td +(3 / 7) Tj +ET +endstream +endobj +9 0 obj +<> +endobj +10 0 obj +<< +/Length 20063 +>> +stream +0. w +0.78 G +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +42. 801.8899999999999864 34. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +47. 790.9399999999999409 Td +(Step) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +76. 801.8899999999999864 145. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +81. 790.9399999999999409 Td +(Tester action) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +221. 801.8899999999999864 234.2800000000000296 -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +226. 790.9399999999999409 Td +(Expected result) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +455.2800000000000296 801.8899999999999864 98. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +460.2800000000000296 790.9399999999999409 Td +(Technical ref) Tj +ET +0.8 0.85 0.85 RG +0.5 w +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 783.8400000000000318 34. -18.7399999999999984 re +B +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 783.8400000000000318 145. -18.7399999999999984 re +B +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +221. 783.8400000000000318 234.2800000000000296 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +226. 772.3799999999999955 Td +(LEFT.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +455.2800000000000296 783.8400000000000318 98. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +460.2800000000000296 772.3799999999999955 Td +(+ cleared fees) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 765.1000000000000227 34. -27.4799999999999969 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 753.6399999999999864 Td +(I1.10) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 765.1000000000000227 145. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 753.6399999999999864 Td +(Deliver the inventory, recording the) Tj +T* (receiver name.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +221. 765.1000000000000227 234.2800000000000296 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +226. 753.6399999999999864 Td +(Status DELIVERED; handovers stamped delivered.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +455.2800000000000296 765.1000000000000227 98. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +460.2800000000000296 753.6399999999999864 Td +(Requires handover signed) Tj +T* (AND truck departed) Tj +ET +0.8 0.85 0.85 RG +0.5 w +0.78 G +0. w +0.8 0.85 0.85 RG +0.5 w +0.8 0.85 0.85 RG +0.5 w +0.97 0.93 0.93 rg +42. 723.6200000000000045 511.2799999999999727 -51. re +f +0.62 0.17 0.2 rg +42. 723.6200000000000045 3. -51. re +f +BT +/F1 8.6 Tf +9.8899999999999988 TL +0.275 0.345 0.357 rg +56. 710.6200000000000045 Td +(THE SINGLE MOST LIKELY BUG YOU WILL HIT. Exit paper returns 400 when the handover is not fully signed, or when a) Tj +T* (warehouse fee invoice is ISSUED / PARTIALLY_PAID. In the container list, the Exit Paper button turns grey and clicking it sends) Tj +T* (the customer a signature request instead of erroring. That is correct behaviour - verify the message, do not file it as a bug.) Tj +ET +BT +/F5 9 Tf +10.3499999999999996 TL +0.043 0.333 0.388 rg +42. 650.6200000000000045 Td +(05) Tj +ET +BT +/F9 17 Tf +19.5499999999999972 TL +0.063 0.102 0.11 rg +72. 649.6200000000000045 Td +(Import - EDR last mile) Tj +ET +0.06 0.1 0.11 RG +0.9 w +42. 629.6200000000000045 m +553.2799999999999727 629.6200000000000045 l +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +42. 615.6200000000000045 Td +(EDR delivers to the customer's door. No customer truck, no portal Approve delivery, and the handover is PER DELIVERING) Tj +T* (TRUCK - not per booking. This is where truck detention accrues.) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +42. 581.6200000000000045 34. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +47. 570.6700000000000728 Td +(Step) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +76. 581.6200000000000045 145. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +81. 570.6700000000000728 Td +(Tester action) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +221. 581.6200000000000045 234.2799999999999727 -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +226. 570.6700000000000728 Td +(Expected result) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +455.2799999999999727 581.6200000000000045 98. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +460.2799999999999727 570.6700000000000728 Td +(Technical ref) Tj +ET +0.06 0.1 0.11 RG +0.9 w +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 563.5699999999999363 34. -36.2199999999999989 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 552.1100000000000136 Td +(I2.1) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 563.5699999999999363 145. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 552.1100000000000136 Td +(Import booking with a last-mile delivery) Tj +T* (address \(or service type including last) Tj +T* (mile\).) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +221. 563.5699999999999363 234.2799999999999727 -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +226. 552.1100000000000136 Td +(hasLastMile is true; no "assign a truck" notification is sent.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +455.2799999999999727 563.5699999999999363 98. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +460.2799999999999727 552.1100000000000136 Td +(EDR haulage - customer) Tj +T* (assigns nothing) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 527.3500000000000227 34. -27.4799999999999969 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 515.8900000000001 Td +(I2.2) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 527.3500000000000227 145. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 515.8900000000001 Td +(Unload from the arrived train, inspect) Tj +T* (PASSED.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +221. 527.3500000000000227 234.2799999999999727 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +226. 515.8900000000001 Td +(Item becomes READY_FOR_PICKUP and the last-mile leg is) Tj +T* (accepted automatically.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +455.2799999999999727 527.3500000000000227 98. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +460.2799999999999727 515.8900000000001 Td +() Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 499.8700000000000045 34. -18.7399999999999984 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 488.410000000000025 Td +(I2.3) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 499.8700000000000045 145. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 488.410000000000025 Td +(Last Mile page: assign vehicle + driver.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +221. 499.8700000000000045 234.2799999999999727 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +226. 488.410000000000025 Td +(Leg reaches READY_TO_TRANSIT. Row shows Assigned.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +455.2799999999999727 499.8700000000000045 98. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +460.2799999999999727 488.410000000000025 Td +(Driver notified by SMS) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 481.1299999999999955 34. -27.4799999999999969 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 469.6700000000000159 Td +(I2.4) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 481.1299999999999955 145. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 469.6700000000000159 Td +(Truck Arrival from the Last Mile row:) Tj +T* (gate-in, tare weight.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +221. 481.1299999999999955 234.2799999999999727 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +226. 469.6700000000000159 Td +(Weighing saved; the assigned last-mile truck is pre-selected.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +455.2799999999999727 481.1299999999999955 98. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +460.2799999999999727 469.6700000000000159 Td +(Re-opening must show) Tj +T* (the saved details) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 453.6499999999999773 34. -27.4799999999999969 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 442.1899999999999977 Td +(I2.5) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 453.6499999999999773 145. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 442.1899999999999977 Td +(Truck Leaving: gate-out, gross weight.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +221. 453.6499999999999773 234.2799999999999727 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +226. 442.1899999999999977 Td +(Release document issued; leg moves to IN_TRANSIT. arrivedAt) Tj +T* (starts the detention clock.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +455.2799999999999727 453.6499999999999773 98. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +460.2799999999999727 442.1899999999999977 Td +(Detention window opens) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 426.1699999999999591 34. -36.2199999999999989 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 414.7099999999999795 Td +(I2.6) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 426.1699999999999591 145. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 414.7099999999999795 Td +(Deliver at the customer's door, recording) Tj +T* (the receiver name.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +221. 426.1699999999999591 234.2799999999999727 -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +226. 414.7099999999999795 Td +(Item DELIVERED. An EDR_LAST_MILE handover is generated) Tj +T* (PER DELIVERING TRUCK, resolved from the container's allocated) Tj +T* (vehicle.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +455.2799999999999727 426.1699999999999591 98. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +460.2799999999999727 414.7099999999999795 Td +(Not booking-level) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 389.9499999999999318 34. -27.4799999999999969 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 378.4899999999999523 Td +(I2.7) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 389.9499999999999318 145. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 378.4899999999999523 Td +(Return the vehicle > mark the leg) Tj +T* (DELIVERED.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +221. 389.9499999999999318 234.2799999999999727 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +226. 378.4899999999999523 Td +(deliveredAt stamped; detention clock stops.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +455.2799999999999727 389.9499999999999318 98. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +460.2799999999999727 378.4899999999999523 Td +() Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 362.4699999999999136 34. -27.4799999999999969 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 351.0099999999999341 Td +(I2.8) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 362.4699999999999136 145. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 351.0099999999999341 Td +(Preview, then generate the truck) Tj +T* (detention invoice.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +221. 362.4699999999999136 234.2799999999999727 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +226. 351.0099999999999341 Td +(Charged per truck per day beyond the grace hours, at the matching) Tj +T* (tier. See section 07.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +455.2799999999999727 362.4699999999999136 98. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +460.2799999999999727 351.0099999999999341 Td +(POST /last-mile/:id/genera) Tj +T* (te-truck-detention-invoice) Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 334.9899999999998954 34. -36.2199999999999989 re +B +BT +/F6 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 323.529999999999859 Td +(I2.G) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +76. 334.9899999999998954 145. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +81. 323.529999999999859 Td +(Open the booking in the portal.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +221. 334.9899999999998954 234.2799999999999727 -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +226. 323.529999999999859 Td +(ABSENT: Approve delivery must NOT appear. The portal flag) Tj +T* (counts only SELF_HAUL handovers; EDR handovers are signed) Tj +T* (by the receiver at the door.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +455.2799999999999727 334.9899999999998954 98. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +460.2799999999999727 323.529999999999859 Td +(Regression check) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.78 G +0. w +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +BT +/F5 9 Tf +10.3499999999999996 TL +0.043 0.333 0.388 rg +42. 272.7699999999998681 Td +(06) Tj +ET +BT +/F9 17 Tf +19.5499999999999972 TL +0.063 0.102 0.11 rg +72. 271.7699999999998681 Td +(Marshalling, gate pass, interchange) Tj +ET +0.06 0.1 0.11 RG +0.9 w +42. 251.7699999999998681 m +553.2799999999999727 251.7699999999998681 l +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +42. 237.7699999999998681 Td +(Documents are generated, not uploaded. Each has a precondition; test the precondition, not just the PDF.) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +42. 215.7699999999998681 92. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +47. 204.8199999999998226 Td +(Document) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +134. 215.7699999999998681 95. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +139. 204.8199999999998226 Td +(When) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +229. 215.7699999999998681 146.1434567681443468 -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +234. 204.8199999999998226 Td +(Precondition) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +375.1434567681443468 215.7699999999998681 178.1365432318555975 -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +380.1434567681443468 204.8199999999998226 Td +(Verify) Tj +ET +0.06 0.1 0.11 RG +0.9 w +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 197.7199999999999136 92. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 186.2599999999998772 Td +(GRN) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +134. 197.7199999999999136 95. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +139. 186.2599999999998772 Td +(On receive to warehouse) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +229. 197.7199999999999136 146.1434567681443468 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +234. 186.2599999999998772 Td +(Booking PAID; route direction matches;) Tj +T* (export needs a truck entrance) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +375.1434567681443468 197.7199999999999136 178.1365432318555975 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +380.1434567681443468 186.2599999999998772 Td +(Number format GRN---<8>;) Tj +T* (PDF opens; customer SMS sent) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 170.2399999999998954 92. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 158.779999999999859 Td +(Import load list /) Tj +T* (marshalling) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +134. 170.2399999999998954 95. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +139. 158.779999999999859 Td +(Import train, before) Tj +T* (unload) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +229. 170.2399999999998954 146.1434567681443468 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +234. 158.779999999999859 Td +(Schedule has bookings assigned) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +375.1434567681443468 170.2399999999998954 178.1365432318555975 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +380.1434567681443468 158.779999999999859 Td +(Summary endpoint and printable PDF agree;) Tj +T* (portrait/landscape both render) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 142.7599999999998772 92. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 131.2999999999998408 Td +(Export marshalling /) Tj +T* (load list) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +134. 142.7599999999998772 95. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +139. 131.2999999999998408 Td +(Export train, after loading) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +229. 142.7599999999998772 146.1434567681443468 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +234. 131.2999999999998408 Td +(Items LOADED onto wagons) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +375.1434567681443468 142.7599999999998772 178.1365432318555975 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +380.1434567681443468 131.2999999999998408 Td +(Wagon, booking, container rows are complete) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 115.279999999999859 92. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 103.8199999999998226 Td +(Gate pass) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +134. 115.279999999999859 95. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +139. 103.8199999999998226 Td +(Djibouti-side operations) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +229. 115.279999999999859 146.1434567681443468 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +234. 103.8199999999998226 Td +(Granted per schedule) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +375.1434567681443468 115.279999999999859 178.1365432318555975 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +380.1434567681443468 103.8199999999998226 Td +(gatepass_granted_at is set; export Djibouti unload) Tj +T* (reads this same field) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 87.7999999999998408 92. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 76.3399999999998045 Td +(Interchange document) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +134. 87.7999999999998408 95. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +139. 76.3399999999998045 Td +(Automatically, after a) Tj +T* (successful Djibouti) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +229. 87.7999999999998408 146.1434567681443468 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +234. 76.3399999999998045 Td +(At least one item unloaded) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +375.1434567681443468 87.7999999999998408 178.1365432318555975 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +380.1434567681443468 76.3399999999998045 Td +(Document number returned in the unload) Tj +T* (response; visible in Interchange Documents) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +0.78 G +0. w +0.8 0.85 0.85 RG +0.5 w +42. 36. m +553.2799999999999727 36. l +S +BT +/F9 8 Tf +9.1999999999999993 TL +0.427 0.498 0.51 rg +42. 22. Td +(EDR Freight - QA Test Plan) Tj +ET +BT +/F5 7.5 Tf +8.625 TL +0.427 0.498 0.51 rg +530.7799999999999727 22. Td +(4 / 7) Tj +ET +endstream +endobj +11 0 obj +<> +endobj +12 0 obj +<< +/Length 13441 +>> +stream +0. w +0.78 G +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +42. 801.8899999999999864 92. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +47. 790.9399999999999409 Td +(Document) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +134. 801.8899999999999864 95. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +139. 790.9399999999999409 Td +(When) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +229. 801.8899999999999864 146.1434567681443468 -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +234. 790.9399999999999409 Td +(Precondition) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +375.1434567681443468 801.8899999999999864 178.1365432318555975 -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +380.1434567681443468 790.9399999999999409 Td +(Verify) Tj +ET +0.06 0.1 0.11 RG +0.9 w +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 783.8400000000000318 92. -18.7399999999999984 re +B +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +134. 783.8400000000000318 95. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +139. 772.3799999999999955 Td +(unload) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +229. 783.8400000000000318 146.1434567681443468 -18.7399999999999984 re +B +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +375.1434567681443468 783.8400000000000318 178.1365432318555975 -18.7399999999999984 re +B +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 765.1000000000000227 92. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 753.6399999999999864 Td +(Handover) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +134. 765.1000000000000227 95. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +139. 753.6399999999999864 Td +(Self-haul: on truck arrival.) Tj +T* (EDR: at delivery.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +229. 765.1000000000000227 146.1434567681443468 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +234. 753.6399999999999864 Td +(See sections 04 / 05) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +375.1434567681443468 765.1000000000000227 178.1365432318555975 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +380.1434567681443468 753.6399999999999864 Td +(Self-haul is booking-level, one per booking; EDR) Tj +T* (is one per delivering truck) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 737.6200000000000045 92. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 726.1599999999999682 Td +(Exit paper / release doc) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +134. 737.6200000000000045 95. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +139. 726.1599999999999682 Td +(Truck leaving) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +229. 737.6200000000000045 146.1434567681443468 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +234. 726.1599999999999682 Td +(Handover fully signed AND no unpaid) Tj +T* (warehouse fee) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +375.1434567681443468 737.6200000000000045 178.1365432318555975 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +380.1434567681443468 726.1599999999999682 Td +(Weights on the paper match the gate weighing) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.78 G +0. w +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +0.97 0.94 0.89 rg +42. 696.1399999999999864 511.2799999999999727 -51. re +f +0.54 0.35 0.04 rg +42. 696.1399999999999864 3. -51. re +f +BT +/F1 8.6 Tf +9.8899999999999988 TL +0.275 0.345 0.357 rg +56. 683.1399999999999864 Td +(CROSS-MODULE QUIRK WORTH CONFIRMING WITH THE TEAM. The EXPORT Djibouti unload checks the gate-pass flag) Tj +T* (stored on the IMPORT Djibouti operations record \(import_djibouti_operations.gatepass_granted_at\). It works, but it is surprising.) Tj +T* (If an export unload silently skips every item, check that field first.) Tj +ET +BT +/F5 9 Tf +10.3499999999999996 TL +0.043 0.333 0.388 rg +42. 623.1399999999999864 Td +(07) Tj +ET +BT +/F9 17 Tf +19.5499999999999972 TL +0.063 0.102 0.11 rg +72. 622.1399999999999864 Td +(Fee rules) Tj +ET +0.06 0.1 0.11 RG +0.9 w +42. 602.1399999999999864 m +553.2799999999999727 602.1399999999999864 l +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +42. 588.1399999999999864 Td +(Four rule types. Two are day-based with free days and tiers; double handling is a flat rate multiplied by a basis; truck detention is) Tj +T* (hour-graced and vehicle-scoped.) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +42. 554.1399999999999864 88. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +47. 543.1900000000000546 Td +(Rule type) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +130. 554.1399999999999864 72. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +135. 543.1900000000000546 Td +(Charged on) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +202. 554.1399999999999864 85. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +207. 543.1900000000000546 Td +(Key fields) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +287. 554.1399999999999864 266.2799999999999727 -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +292. 543.1900000000000546 Td +(Test cases) Tj +ET +0.06 0.1 0.11 RG +0.9 w +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 536.0899999999999181 88. -27.4799999999999969 re +B +BT +/F5 7 Tf +8.0499999999999989 TL +0.063 0.102 0.11 rg +47. 525.1399999999999864 Td +(STORAGE_FEE) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +130. 536.0899999999999181 72. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +135. 524.6299999999999955 Td +(Days in storage) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +202. 536.0899999999999181 85. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +207. 524.6299999999999955 Td +(free days, tiers) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +287. 536.0899999999999181 266.2799999999999727 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +292. 524.6299999999999955 Td +(Within free days -> zero. One day past -> tier 1. Cross a tier boundary ->) Tj +T* (correct tier rate.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 508.6099999999999568 88. -27.4799999999999969 re +B +BT +/F5 7 Tf +8.0499999999999989 TL +0.063 0.102 0.11 rg +47. 497.6599999999999682 Td +(DEMURRAGE_FEE) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +130. 508.6099999999999568 72. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +135. 497.1499999999999773 Td +(Days beyond free) Tj +T* (time) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +202. 508.6099999999999568 85. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +207. 497.1499999999999773 Td +(free days, tiers) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +287. 508.6099999999999568 266.2799999999999727 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +292. 497.1499999999999773 Td +(Same boundary tests. Confirm it blocks exit paper and delivery while) Tj +T* (ISSUED.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 481.1299999999999386 88. -44.9599999999999937 re +B +BT +/F5 7 Tf +8.0499999999999989 TL +0.063 0.102 0.11 rg +47. 470.17999999999995 Td +(DOUBLE_HANDLING_FE) Tj +T* (E) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +130. 481.1299999999999386 72. -44.9599999999999937 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +135. 469.6699999999999591 Td +(Flat rate x quantity) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +202. 481.1299999999999386 85. -44.9599999999999937 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +207. 469.6699999999999591 Td +(basis:) Tj +T* (PER_CONTAINER |) Tj +T* (PER_TON |) Tj +T* (PER_ITEM) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +287. 481.1299999999999386 266.2799999999999727 -44.9599999999999937 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +292. 469.6699999999999591 Td +(Container booking -> PER_CONTAINER uses container count. Bulk ->) Tj +T* (PER_TON uses tonnage. Break-bulk machinery -> PER_ITEM uses item) Tj +T* (count. IMPORT ONLY. Free days and tiers must not apply.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 436.1699999999999591 88. -36.2199999999999989 re +B +BT +/F5 7 Tf +8.0499999999999989 TL +0.063 0.102 0.11 rg +47. 425.2199999999999704 Td +(TRUCK_DETENTION_FE) Tj +T* (E) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +130. 436.1699999999999591 72. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +135. 424.7099999999999795 Td +(Per truck, per day) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +202. 436.1699999999999591 85. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +207. 424.7099999999999795 Td +(free_hours grace,) Tj +T* (tiers, vehicle type) Tj +T* (scope) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +287. 436.1699999999999591 266.2799999999999727 -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +292. 424.7099999999999795 Td +(Return inside the grace window -> zero. Just past grace -> day 1 at tier 1.) Tj +T* (Multi-day -> tier escalation. A vehicle type outside the rule's scope -> no) Tj +T* (charge. IMPORT ONLY.) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.78 G +0. w +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +BT +/F2 8 Tf +9.1999999999999993 TL +0.063 0.102 0.11 rg +42. 377.9499999999999318 Td +(FEE BEHAVIOUR TO VERIFY ON EVERY RULE) Tj +ET +0.43 0.5 0.51 RG +0.6 w +43. 373.4499999999999318 8. -8. re +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +60. 365.9499999999999318 Td +(Preview before invoice. The preview amount must equal the issued invoice total.) Tj +ET +0.43 0.5 0.51 RG +0.6 w +43. 356.4499999999999318 8. -8. re +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +60. 348.9499999999999318 Td +(Notification on issue. Issuing a warehouse fee invoice sends the customer an in-app INVOICE_ISSUED notification AND) Tj +T* (an SMS, deep-linked to pay.) Tj +ET +0.43 0.5 0.51 RG +0.6 w +43. 327.4499999999999318 8. -8. re +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +60. 319.9499999999999318 Td +(Clearance gate. While a warehouse-source invoice is ISSUED or PARTIALLY_PAID, exit paper, terminal release, and) Tj +T* (Approve delivery are all blocked.) Tj +ET +0.43 0.5 0.51 RG +0.6 w +43. 298.4499999999999318 8. -8. re +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +60. 290.9499999999999318 Td +(Payable-but-uninvoiced. If fees are payable and no invoice exists yet, release is still blocked with "Generate and fully) Tj +T* (pay...". Confirm the operator can generate it from that state.) Tj +ET +0.43 0.5 0.51 RG +0.6 w +43. 269.4499999999999318 8. -8. re +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +60. 261.9499999999999318 Td +(Fully paid -> release proceeds; a receipt PDF is available.) Tj +ET +0.43 0.5 0.51 RG +0.6 w +43. 252.4499999999999318 8. -8. re +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +60. 244.9499999999999318 Td +(Edit a rule \(rate, free days, tiers, grace hours\) and confirm the next preview reflects it.) Tj +ET +BT +/F5 9 Tf +10.3499999999999996 TL +0.043 0.333 0.388 rg +42. 211.9499999999999318 Td +(08) Tj +ET +BT +/F9 17 Tf +19.5499999999999972 TL +0.063 0.102 0.11 rg +72. 210.9499999999999318 Td +(Allocation rules) Tj +ET +0.06 0.1 0.11 RG +0.9 w +42. 190.9499999999999318 m +553.2799999999999727 190.9499999999999318 l +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +42. 176.9499999999999318 Td +(Where an item is stored is decided by the first matching rule, in priority order. Test the precedence, not just one rule.) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +42. 154.9499999999999318 258.0206829268292381 -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +47. 143.9999999999998863 Td +(Match criteria \(any may be null = wildcard\)) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +300.0206829268292381 154.9499999999999318 253.2593170731707346 -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +305.0206829268292381 143.9999999999998863 Td +(Targets) Tj +ET +0.06 0.1 0.11 RG +0.9 w +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 136.8999999999999773 258.0206829268292381 -34.1499999999999986 re +B +BT +/F5 7 Tf +8.0499999999999989 TL +0.063 0.102 0.11 rg +47. 125.9499999999999318 Td +(freight_type, trade_direction, cargo_type_code,) Tj +T* (container_status, requires_inspection, ordered by priority) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +300.0206829268292381 136.8999999999999773 253.2593170731707346 -34.1499999999999986 re +B +BT +/F5 7 Tf +8.0499999999999989 TL +0.275 0.345 0.357 rg +305.0206829268292381 125.9499999999999318 Td +(target_facility_code, target_warehouse_code,) Tj +T* (target_yard_code \(required\), target_zone_code,) Tj +T* (storage_type) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.78 G +0. w +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +BT +/F2 8 Tf +9.1999999999999993 TL +0.063 0.102 0.11 rg +42. 80.75 Td +(PRECEDENCE, HIGHEST FIRST) Tj +ET +0.8 0.85 0.85 RG +0.5 w +42. 36. m +553.2799999999999727 36. l +S +BT +/F9 8 Tf +9.1999999999999993 TL +0.427 0.498 0.51 rg +42. 22. Td +(EDR Freight - QA Test Plan) Tj +ET +BT +/F5 7.5 Tf +8.625 TL +0.427 0.498 0.51 rg +530.7799999999999727 22. Td +(5 / 7) Tj +ET +endstream +endobj +13 0 obj +<> +endobj +14 0 obj +<< +/Length 21215 +>> +stream +0.9 w +0.06 0.1 0.11 RG +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +42. 793.8899999999999864 20. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +47. 782.9399999999999409 Td +(#) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +62. 793.8899999999999864 120. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +67. 782.9399999999999409 Td +(Source of the location) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +182. 793.8899999999999864 207.6664663121432 -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +187. 782.9399999999999409 Td +(How to trigger) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +389.6664663121432 793.8899999999999864 163.6135336878567728 -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +394.6664663121432 782.9399999999999409 Td +(Note recorded) Tj +ET +0.06 0.1 0.11 RG +0.9 w +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 775.8400000000000318 20. -18.7399999999999984 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 764.3799999999999955 Td +(1) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +62. 775.8400000000000318 120. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +67. 764.3799999999999955 Td +(Operator selection) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +182. 775.8400000000000318 207.6664663121432 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +187. 764.3799999999999955 Td +(Store modal > pick warehouse + yard + zone) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +389.6664663121432 775.8400000000000318 163.6135336878567728 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +394.6664663121432 764.3799999999999955 Td +("Stored at operator-selected location") Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 757.1000000000000227 20. -18.7399999999999984 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 745.6399999999999864 Td +(2) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +62. 757.1000000000000227 120. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +67. 745.6399999999999864 Td +(Allocation rule) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +182. 757.1000000000000227 207.6664663121432 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +187. 745.6399999999999864 Td +(Leave the Store modal blank; a matching rule exists) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +389.6664663121432 757.1000000000000227 163.6135336878567728 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +394.6664663121432 745.6399999999999864 Td +("Stored by allocation rule ") Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 738.3600000000000136 20. -18.7399999999999984 re +B +BT +/F5 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 726.8999999999999773 Td +(3) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +62. 738.3600000000000136 120. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +67. 726.8999999999999773 Td +(Capacity-balanced fallback) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +182. 738.3600000000000136 207.6664663121432 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +187. 726.8999999999999773 Td +(Leave blank; no rule matches) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +389.6664663121432 738.3600000000000136 163.6135336878567728 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +394.6664663121432 726.8999999999999773 Td +("Stored by capacity-balanced allocation") Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.78 G +0. w +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +0.43 0.5 0.51 RG +0.6 w +43. 711.1200000000000045 8. -8. re +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +60. 703.6200000000000045 Td +(Two matching rules -> the LOWER priority number wins.) Tj +ET +0.43 0.5 0.51 RG +0.6 w +43. 694.1200000000000045 8. -8. re +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +60. 686.6200000000000045 Td +(Yard dropdowns are filtered by freight type - container bookings offer container yards only.) Tj +ET +0.43 0.5 0.51 RG +0.6 w +43. 677.1200000000000045 8. -8. re +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +60. 669.6200000000000045 Td +(Only ACTIVE warehouses, yards and zones are selectable.) Tj +ET +0.43 0.5 0.51 RG +0.6 w +43. 660.1200000000000045 8. -8. re +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +60. 652.6200000000000045 Td +(Storing beyond a zone's capacity is rejected; capacities are compared in TONNES.) Tj +ET +0.43 0.5 0.51 RG +0.6 w +43. 643.1200000000000045 8. -8. re +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +60. 635.6200000000000045 Td +(Move an item to another warehouse/yard/zone -> capacity released at source, taken at destination.) Tj +ET +0.43 0.5 0.51 RG +0.6 w +43. 626.1200000000000045 8. -8. re +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +60. 618.6200000000000045 Td +(Edit a rule, an existing warehouse, a yard, and a zone - all four must be editable.) Tj +ET +BT +/F5 9 Tf +10.3499999999999996 TL +0.043 0.333 0.388 rg +42. 585.6200000000000045 Td +(09) Tj +ET +BT +/F9 17 Tf +19.5499999999999972 TL +0.063 0.102 0.11 rg +72. 584.6200000000000045 Td +(Negative & guard cases) Tj +ET +0.06 0.1 0.11 RG +0.9 w +42. 564.6200000000000045 m +553.2799999999999727 564.6200000000000045 l +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +42. 550.6200000000000045 Td +(Every row here is intended behaviour. The test passes when the action is REFUSED with the stated message. Anything that) Tj +T* (succeeds is the bug.) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +42. 516.6200000000000045 74. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +47. 505.6700000000000159 Td +(Area) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +116. 516.6200000000000045 195. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +121. 505.6700000000000159 Td +(Attempt) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +311. 516.6200000000000045 242.2800000000000296 -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +316. 505.6700000000000159 Td +(Expected refusal) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 498.5699999999999932 74. -27.4799999999999969 re +B +BT +/F2 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 487.1100000000000136 Td +(Booking) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +116. 498.5699999999999932 195. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +121. 487.1100000000000136 Td +(Enter a container number that is not 4 letters + 7 digits) Tj +T* (\(e.g. MSKU10105185, 3456789\).) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +311. 498.5699999999999932 242.2800000000000296 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +316. 487.1100000000000136 Td +("Use ISO format: 4 letters + 7 digits". Lowercase is auto-uppercased;) Tj +T* (input capped at 11 characters.) Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 471.089999999999975 74. -18.7399999999999984 re +B +BT +/F2 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 459.6299999999999955 Td +(Receive) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +116. 471.089999999999975 195. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +121. 459.6299999999999955 Td +(Receive a booking that is not PAID.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +311. 471.089999999999975 242.2800000000000296 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +316. 459.6299999999999955 Td +(Skipped: "Booking not PAID".) Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 452.3499999999999659 74. -18.7399999999999984 re +B +BT +/F2 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 440.8899999999999864 Td +(Receive) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +116. 452.3499999999999659 195. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +121. 440.8899999999999864 Td +(Receive the same booking twice.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +311. 452.3499999999999659 242.2800000000000296 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +316. 440.8899999999999864 Td +(Skipped: "Already received".) Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 433.6099999999999568 74. -18.7399999999999984 re +B +BT +/F2 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 422.1499999999999773 Td +(Receive) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +116. 433.6099999999999568 195. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +121. 422.1499999999999773 Td +(Export receive with no truck entrance captured.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +311. 433.6099999999999568 242.2800000000000296 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +316. 422.1499999999999773 Td +(Rejected before any inventory is created.) Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 414.8699999999999477 74. -18.7399999999999984 re +B +BT +/F2 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 403.4099999999999682 Td +(Truck assign) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +116. 414.8699999999999477 195. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +121. 403.4099999999999682 Td +(Put two 40ft containers on one truck.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +311. 414.8699999999999477 242.2800000000000296 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +316. 403.4099999999999682 Td +("A 40ft container fills the truck - assign only 1 container to this truck".) Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 396.1299999999999386 74. -18.7399999999999984 re +B +BT +/F2 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 384.6699999999999591 Td +(Truck assign) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +116. 396.1299999999999386 195. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +121. 384.6699999999999591 Td +(Put three containers on one truck.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +311. 396.1299999999999386 242.2800000000000296 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +316. 384.6699999999999591 Td +("A truck carries at most 2 containers".) Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 377.3899999999999295 74. -18.7399999999999984 re +B +BT +/F2 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 365.92999999999995 Td +(Truck assign) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +116. 377.3899999999999295 195. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +121. 365.92999999999995 Td +(Assign more trucks than the booking has containers.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +311. 377.3899999999999295 242.2800000000000296 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +316. 365.92999999999995 Td +("Cannot assign more trucks than containers...".) Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 358.6499999999999204 74. -27.4799999999999969 re +B +BT +/F2 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 347.1899999999999409 Td +(Truck assign) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +116. 358.6499999999999204 195. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +121. 347.1899999999999409 Td +(Assign a container from another booking, or one) Tj +T* (already on another truck.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +311. 358.6499999999999204 242.2800000000000296 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +316. 347.1899999999999409 Td +("...is not one of this booking's containers" / "...already loaded onto) Tj +T* (another truck".) Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 331.1699999999999022 74. -18.7399999999999984 re +B +BT +/F2 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 319.7099999999999227 Td +(Truck assign) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +116. 331.1699999999999022 195. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +121. 319.7099999999999227 Td +(Edit a truck AFTER it has arrived.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +311. 331.1699999999999022 242.2800000000000296 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +316. 319.7099999999999227 Td +(Refused - edits are allowed only until arrival.) Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 312.42999999999995 74. -27.4799999999999969 re +B +BT +/F2 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 300.9699999999999136 Td +(Loading) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +116. 312.42999999999995 195. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +121. 300.9699999999999136 Td +(Load containers onto a truck that has not arrived.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +311. 312.42999999999995 242.2800000000000296 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +316. 300.9699999999999136 Td +("Record the truck arrival before loading...". The truck picker lists only) Tj +T* (arrived, not-departed trucks.) Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 284.9499999999999318 74. -18.7399999999999984 re +B +BT +/F2 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 273.4899999999998954 Td +(Loading) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +116. 284.9499999999999318 195. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +121. 273.4899999999998954 Td +(Load onto a truck that has already departed.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +311. 284.9499999999999318 242.2800000000000296 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +316. 273.4899999999998954 Td +("This truck has already left - its load is locked".) Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 266.2099999999999227 74. -27.4799999999999969 re +B +BT +/F2 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 254.7499999999998863 Td +(Stages) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +116. 266.2099999999999227 195. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +121. 254.7499999999998863 Td +(Customer assigns containers to a truck, then check the) Tj +T* (container list.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +311. 266.2099999999999227 242.2800000000000296 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +316. 254.7499999999998863 Td +(Stage is ASSIGNED, NEVER LOADED. Exit Paper is not offered.) Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 238.7299999999999045 74. -27.4799999999999969 re +B +BT +/F2 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 227.2699999999998681 Td +(Truck leaving) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +116. 238.7299999999999045 195. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +121. 227.2699999999998681 Td +(Enter a gross weight where gross - tare != the selected) Tj +T* (containers' cargo weight.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +311. 238.7299999999999045 242.2800000000000296 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +316. 227.2699999999998681 Td +("Weight mismatch...". Exit paper and gate clearance blocked, client) Tj +T* (and server.) Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 211.2499999999998863 74. -18.7399999999999984 re +B +BT +/F2 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 199.7899999999998499 Td +(Truck leaving) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +116. 211.2499999999998863 195. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +121. 199.7899999999998499 Td +(Save leaving with no containers selected.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +311. 211.2499999999998863 242.2800000000000296 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +316. 199.7899999999998499 Td +("Select the containers loaded on this truck".) Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 192.5099999999998772 74. -27.4799999999999969 re +B +BT +/F2 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 181.0499999999998408 Td +(Exit paper) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +116. 192.5099999999998772 195. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +121. 181.0499999999998408 Td +(Generate before the handover is signed.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +311. 192.5099999999998772 242.2800000000000296 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +316. 181.0499999999998408 Td +("Handover must be signed...". In the container list the button is grey) Tj +T* (and instead SENDS THE CUSTOMER A SIGNATURE REQUEST.) Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 165.029999999999859 74. -18.7399999999999984 re +B +BT +/F2 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 153.5699999999998226 Td +(Exit paper) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +116. 165.029999999999859 195. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +121. 153.5699999999998226 Td +(Generate with an ISSUED demurrage/storage invoice.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +311. 165.029999999999859 242.2800000000000296 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +316. 153.5699999999998226 Td +("...must be fully paid before terminal release".) Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 146.2899999999998499 74. -27.4799999999999969 re +B +BT +/F2 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 134.8299999999998136 Td +(Approve delivery) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +116. 146.2899999999998499 195. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +121. 134.8299999999998136 Td +(Approve without a saved signature.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +311. 146.2899999999998499 242.2800000000000296 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +316. 134.8299999999998136 Td +("Please save your signature..." and the portal routes to the signature) Tj +T* (page.) Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 118.8099999999998317 74. -18.7399999999999984 re +B +BT +/F2 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 107.3499999999997954 Td +(Approve delivery) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +116. 118.8099999999998317 195. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +121. 107.3499999999997954 Td +(Approve before warehouse inspection has passed.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +311. 118.8099999999998317 242.2800000000000296 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +316. 107.3499999999997954 Td +("Delivery can be approved after warehouse inspection has passed".) Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 100.0699999999998226 74. -18.7399999999999984 re +B +BT +/F2 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 88.6099999999997863 Td +(Approve delivery) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +116. 100.0699999999998226 195. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +121. 88.6099999999997863 Td +(Approve when a truck is assigned but has not arrived.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +311. 100.0699999999998226 242.2800000000000296 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +316. 88.6099999999997863 Td +("Customer truck arrival must be recorded before delivery approval".) Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 81.3299999999998136 74. -27.4799999999999969 re +B +BT +/F2 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 69.8699999999997772 Td +(Deliver) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +116. 81.3299999999998136 195. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +121. 69.8699999999997772 Td +(Deliver before a release order was issued, or before) Tj +T* (the self-haul truck has left.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +311. 81.3299999999998136 242.2800000000000296 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +316. 69.8699999999997772 Td +("A release order must be issued..." / "Deliver is available only after) Tj +T* (the customer truck has left".) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +0.78 G +0. w +0.8 0.85 0.85 RG +0.5 w +42. 36. m +553.2799999999999727 36. l +S +BT +/F9 8 Tf +9.1999999999999993 TL +0.427 0.498 0.51 rg +42. 22. Td +(EDR Freight - QA Test Plan) Tj +ET +BT +/F5 7.5 Tf +8.625 TL +0.427 0.498 0.51 rg +530.7799999999999727 22. Td +(6 / 7) Tj +ET +endstream +endobj +15 0 obj +<> +endobj +16 0 obj +<< +/Length 15025 +>> +stream +0. w +0.78 G +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +42. 801.8899999999999864 74. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +47. 790.9399999999999409 Td +(Area) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +116. 801.8899999999999864 195. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +121. 790.9399999999999409 Td +(Attempt) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +311. 801.8899999999999864 242.2800000000000296 -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +316. 790.9399999999999409 Td +(Expected refusal) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 783.8400000000000318 74. -18.7399999999999984 re +B +BT +/F2 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 772.3799999999999955 Td +(Djibouti unload) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +116. 783.8400000000000318 195. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +121. 772.3799999999999955 Td +(Unload an export train with no gate pass granted.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +311. 783.8400000000000318 242.2800000000000296 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +316. 772.3799999999999955 Td +(Items skipped; no interchange document generated.) Tj +ET +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 765.1000000000000227 74. -27.4799999999999969 re +B +BT +/F2 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 753.6399999999999864 Td +(Warehouse) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +116. 765.1000000000000227 195. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +121. 753.6399999999999864 Td +(Store into an INACTIVE warehouse/yard/zone, or) Tj +T* (beyond capacity.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +311. 765.1000000000000227 242.2800000000000296 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +316. 753.6399999999999864 Td +(Not selectable / "No active warehouse yard/zone is available" /) Tj +T* (capacity error.) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.78 G +0. w +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +BT +/F5 9 Tf +10.3499999999999996 TL +0.043 0.333 0.388 rg +42. 711.6200000000000045 Td +(10) Tj +ET +BT +/F9 17 Tf +19.5499999999999972 TL +0.063 0.102 0.11 rg +72. 710.6200000000000045 Td +(Notifications) Tj +ET +0.06 0.1 0.11 RG +0.9 w +42. 690.6200000000000045 m +553.2799999999999727 690.6200000000000045 l +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +42. 676.6200000000000045 Td +(All customer notifications land in the same portal inbox. Verify the message, the deep link, and - where noted - the SMS and) Tj +T* (email.) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +42. 642.6200000000000045 96. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +47. 631.6700000000000728 Td +(Notification) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +138. 642.6200000000000045 257.2800000000000296 -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +143. 631.6700000000000728 Td +(Fires when) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +395.2800000000000296 642.6200000000000045 74. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +400.2800000000000296 631.6700000000000728 Td +(Channels) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +469.2800000000000296 642.6200000000000045 84. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +474.2800000000000296 631.6700000000000728 Td +(Deep link) Tj +ET +0.06 0.1 0.11 RG +0.9 w +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 624.5699999999999363 96. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 613.1100000000000136 Td +(Shipment dispatched) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +138. 624.5699999999999363 257.2800000000000296 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +143. 613.1100000000000136 Td +(Train schedule dispatched, per booking) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +395.2800000000000296 624.5699999999999363 74. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +400.2800000000000296 613.1100000000000136 Td +(In-app, SMS, email) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +469.2800000000000296 624.5699999999999363 84. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +474.2800000000000296 613.1100000000000136 Td +(Booking) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 605.8299999999999272 96. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 594.3700000000000045 Td +(Shipment arrived) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +138. 605.8299999999999272 257.2800000000000296 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +143. 594.3700000000000045 Td +(Train schedule arrived, per booking) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +395.2800000000000296 605.8299999999999272 74. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +400.2800000000000296 594.3700000000000045 Td +(In-app, SMS, email) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +469.2800000000000296 605.8299999999999272 84. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +474.2800000000000296 594.3700000000000045 Td +(Booking) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 587.0899999999999181 96. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 575.6299999999999955 Td +(Assign a truck for pickup) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +138. 587.0899999999999181 257.2800000000000296 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +143. 575.6299999999999955 Td +(Export: on warehouse receive. Import: on inspection pass -> ready for) Tj +T* (pickup. Only if self-haul AND no truck assigned.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +395.2800000000000296 587.0899999999999181 74. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +400.2800000000000296 575.6299999999999955 Td +(In-app, SMS, email) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +469.2800000000000296 587.0899999999999181 84. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +474.2800000000000296 575.6299999999999955 Td +(Booking > assign) Tj +T* (truck) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 559.6099999999999 96. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 548.1499999999999773 Td +(Handover - signature) Tj +T* (needed) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +138. 559.6099999999999 257.2800000000000296 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +143. 548.1499999999999773 Td +(Self-haul truck arrives \(handover generated\), and re-sent when an) Tj +T* (operator requests a signature from the Exit Paper button.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +395.2800000000000296 559.6099999999999 74. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +400.2800000000000296 548.1499999999999773 Td +(In-app, SMS, email) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +469.2800000000000296 559.6099999999999 84. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +474.2800000000000296 548.1499999999999773 Td +(Booking > approve) Tj +T* (delivery) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 532.1299999999998818 96. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 520.6699999999999591 Td +(Warehouse fee due) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +138. 532.1299999999998818 257.2800000000000296 -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +143. 520.6699999999999591 Td +(Storage / demurrage invoice issued) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +395.2800000000000296 532.1299999999998818 74. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +400.2800000000000296 520.6699999999999591 Td +(In-app, SMS) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +469.2800000000000296 532.1299999999998818 84. -18.7399999999999984 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +474.2800000000000296 520.6699999999999591 Td +(Booking > pay) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 513.3899999999998727 96. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 501.92999999999995 Td +(Wagon allocated /) Tj +T* (payment window) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +138. 513.3899999999998727 257.2800000000000296 -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +143. 501.92999999999995 Td +(Scheduling) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +395.2800000000000296 513.3899999999998727 74. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +400.2800000000000296 501.92999999999995 Td +(In-app) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +469.2800000000000296 513.3899999999998727 84. -27.4799999999999969 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +474.2800000000000296 501.92999999999995 Td +(Booking) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.78 G +0. w +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +0.97 0.93 0.93 rg +42. 471.9099999999999113 511.2799999999999727 -62. re +f +0.62 0.17 0.2 rg +42. 471.9099999999999113 3. -62. re +f +BT +/F1 8.6 Tf +9.8899999999999988 TL +0.275 0.345 0.357 rg +56. 458.9099999999999113 Td +(DO NOT CHASE A MISSING IN-APP NOTIFICATION BEFORE CHECKING THIS. Recipients are resolved from the company's) Tj +T* (linked portal users. If a company has none, notify\(\) logs "0 recipients - skipped" and stores nothing - the notification simply never) Tj +T* (appears, with no error. SMS and email still go out, because they address the company's phone and email directly. On a fresh) Tj +T* (environment this is the usual explanation.) Tj +ET +BT +/F5 9 Tf +10.3499999999999996 TL +0.043 0.333 0.388 rg +42. 387.9099999999999113 Td +(11) Tj +ET +BT +/F9 17 Tf +19.5499999999999972 TL +0.063 0.102 0.11 rg +72. 386.9099999999999113 Td +(Known open issues) Tj +ET +0.06 0.1 0.11 RG +0.9 w +42. 366.9099999999999113 m +553.2799999999999727 366.9099999999999113 l +S +BT +/F1 9 Tf +10.3499999999999996 TL +0.275 0.345 0.357 rg +42. 352.9099999999999113 Td +(Do not raise duplicates for these. Each is already identified.) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +42. 330.9099999999999113 78. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +47. 319.9599999999999227 Td +(Status) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +120. 330.9099999999999113 190. -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +125. 319.9599999999999227 Td +(Issue) Tj +ET +0.93 0.95 0.95 rg +0.8 0.85 0.85 RG +0.4 w +0.93 0.95 0.95 rg +310. 330.9099999999999113 243.2799999999999727 -18.0499999999999972 re +B +BT +/F2 7 Tf +8.0499999999999989 TL +0.427 0.498 0.51 rg +315. 319.9599999999999227 Td +(Impact on testing) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.97 0.93 0.93 rg +0.8 0.85 0.85 RG +0.4 w +0.97 0.93 0.93 rg +42. 312.8599999999999 78. -36.2199999999999989 re +B +BT +/F2 7.6 Tf +8.7399999999999984 TL +0.616 0.169 0.196 rg +47. 301.3999999999998636 Td +(OPEN) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +120. 312.8599999999999 190. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +125. 301.3999999999998636 Td +(Export receive returns 500 on the deployed) Tj +T* (environment \(POST) Tj +T* (/warehouse-inventory/receive-bulk\).) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +310. 312.8599999999999 243.2799999999999727 -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +315. 301.3999999999998636 Td +(Blocks flows E1 and E2 at step .2. Awaiting the response body /) Tj +T* (server log to diagnose. Likely schema drift, not the SQL.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 276.6399999999998727 78. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 265.1799999999998363 Td +(FIX PENDING) Tj +T* (DEPLOY) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +120. 276.6399999999998727 190. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +125. 265.1799999999998363 Td +(Export receive was extremely slow. The owner SMS) Tj +T* (was awaited inside the DB transaction, and the SMS) Tj +T* (client had no HTTP timeout.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +310. 276.6399999999998727 243.2799999999999727 -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +315. 265.1799999999998363 Td +(Fixed on branch: SMS now has a timeout, and notifications are sent) Tj +T* (after commit. Re-test receive latency once deployed.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 240.4199999999998454 78. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 228.959999999999809 Td +(DATA) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +120. 240.4199999999998454 190. -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +125. 228.959999999999809 Td +(Drivers table has no unique constraints on licence) Tj +T* (number, email, or phone, despite the entity declaring) Tj +T* (them unique.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +310. 240.4199999999998454 243.2799999999999727 -36.2199999999999989 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +315. 228.959999999999809 Td +(Duplicate drivers can be created. Do not rely on uniqueness in test) Tj +T* (assertions.) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +42. 204.1999999999998181 78. -53.6999999999999886 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.063 0.102 0.11 rg +47. 192.7399999999997817 Td +(BEHAVIOUR) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +120. 204.1999999999998181 190. -53.6999999999999886 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +125. 192.7399999999997817 Td +(Handover generated before truck arrival. If an) Tj +T* (operator triggers a signature request from the Exit) Tj +T* (Paper button while a truck is assigned but not arrived,) Tj +T* (Approve delivery refuses with "truck arrival must be) Tj +T* (recorded".) Tj +ET +1. g +0.8 0.85 0.85 RG +0.4 w +1. g +310. 204.1999999999998181 243.2799999999999727 -53.6999999999999886 re +B +BT +/F1 7.6 Tf +8.7399999999999984 TL +0.275 0.345 0.357 rg +315. 192.7399999999997817 Td +(Only reachable off the normal path. Follow flow I1 in order and it will) Tj +T* (not occur.) Tj +ET +0.06 0.1 0.11 RG +0.9 w +0.78 G +0. w +0.06 0.1 0.11 RG +0.9 w +0.06 0.1 0.11 RG +0.9 w +0.8 0.85 0.85 RG +0.5 w +42. 36. m +553.2799999999999727 36. l +S +BT +/F9 8 Tf +9.1999999999999993 TL +0.427 0.498 0.51 rg +42. 22. Td +(EDR Freight - QA Test Plan) Tj +ET +BT +/F5 7.5 Tf +8.625 TL +0.427 0.498 0.51 rg +530.7799999999999727 22. Td +(7 / 7) Tj +ET +endstream +endobj +1 0 obj +<> +endobj +17 0 obj +<< +/Type /Font +/BaseFont /Helvetica +/Subtype /Type1 +/Encoding /WinAnsiEncoding +/FirstChar 32 +/LastChar 255 +>> +endobj +18 0 obj +<< +/Type /Font +/BaseFont /Helvetica-Bold +/Subtype /Type1 +/Encoding /WinAnsiEncoding +/FirstChar 32 +/LastChar 255 +>> +endobj +19 0 obj +<< +/Type /Font +/BaseFont /Helvetica-Oblique +/Subtype /Type1 +/Encoding /WinAnsiEncoding +/FirstChar 32 +/LastChar 255 +>> +endobj +20 0 obj +<< +/Type /Font +/BaseFont /Helvetica-BoldOblique +/Subtype /Type1 +/Encoding /WinAnsiEncoding +/FirstChar 32 +/LastChar 255 +>> +endobj +21 0 obj +<< +/Type /Font +/BaseFont /Courier +/Subtype /Type1 +/Encoding /WinAnsiEncoding +/FirstChar 32 +/LastChar 255 +>> +endobj +22 0 obj +<< +/Type /Font +/BaseFont /Courier-Bold +/Subtype /Type1 +/Encoding /WinAnsiEncoding +/FirstChar 32 +/LastChar 255 +>> +endobj +23 0 obj +<< +/Type /Font +/BaseFont /Courier-Oblique +/Subtype /Type1 +/Encoding /WinAnsiEncoding +/FirstChar 32 +/LastChar 255 +>> +endobj +24 0 obj +<< +/Type /Font +/BaseFont /Courier-BoldOblique +/Subtype /Type1 +/Encoding /WinAnsiEncoding +/FirstChar 32 +/LastChar 255 +>> +endobj +25 0 obj +<< +/Type /Font +/BaseFont /Times-Roman +/Subtype /Type1 +/Encoding /WinAnsiEncoding +/FirstChar 32 +/LastChar 255 +>> +endobj +26 0 obj +<< +/Type /Font +/BaseFont /Times-Bold +/Subtype /Type1 +/Encoding /WinAnsiEncoding +/FirstChar 32 +/LastChar 255 +>> +endobj +27 0 obj +<< +/Type /Font +/BaseFont /Times-Italic +/Subtype /Type1 +/Encoding /WinAnsiEncoding +/FirstChar 32 +/LastChar 255 +>> +endobj +28 0 obj +<< +/Type /Font +/BaseFont /Times-BoldItalic +/Subtype /Type1 +/Encoding /WinAnsiEncoding +/FirstChar 32 +/LastChar 255 +>> +endobj +29 0 obj +<< +/Type /Font +/BaseFont /ZapfDingbats +/Subtype /Type1 +/FirstChar 32 +/LastChar 255 +>> +endobj +30 0 obj +<< +/Type /Font +/BaseFont /Symbol +/Subtype /Type1 +/FirstChar 32 +/LastChar 255 +>> +endobj +2 0 obj +<< +/ProcSet [/PDF /Text /ImageB /ImageC /ImageI] +/Font << +/F1 17 0 R +/F2 18 0 R +/F3 19 0 R +/F4 20 0 R +/F5 21 0 R +/F6 22 0 R +/F7 23 0 R +/F8 24 0 R +/F9 25 0 R +/F10 26 0 R +/F11 27 0 R +/F12 28 0 R +/F13 29 0 R +/F14 30 0 R +>> +/XObject << +>> +>> +endobj +31 0 obj +<< +/Producer (jsPDF 4.2.1) +/CreationDate (D:20260709090321-00'00') +>> +endobj +32 0 obj +<< +/Type /Catalog +/Pages 1 0 R +/OpenAction [3 0 R /FitH null] +/PageLayout /OneColumn +>> +endobj +xref +0 33 +0000000000 65535 f +0000115749 00000 n +0000117610 00000 n +0000000015 00000 n +0000000152 00000 n +0000010120 00000 n +0000010257 00000 n +0000026401 00000 n +0000026538 00000 n +0000045234 00000 n +0000045372 00000 n +0000065489 00000 n +0000065628 00000 n +0000079123 00000 n +0000079262 00000 n +0000100531 00000 n +0000100670 00000 n +0000115845 00000 n +0000115971 00000 n +0000116102 00000 n +0000116236 00000 n +0000116374 00000 n +0000116498 00000 n +0000116627 00000 n +0000116759 00000 n +0000116895 00000 n +0000117023 00000 n +0000117150 00000 n +0000117279 00000 n +0000117412 00000 n +0000117514 00000 n +0000117863 00000 n +0000117949 00000 n +trailer +<< +/Size 33 +/Root 32 0 R +/Info 31 0 R +/ID [ <3BEB58B86E90D0C327872D7AEEEBDAFB> <3BEB58B86E90D0C327872D7AEEEBDAFB> ] +>> +startxref +118053 +%%EOF \ No newline at end of file From cec544fc12c9150dafcd93514b7831cd58a5a805 Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Thu, 9 Jul 2026 15:28:20 +0300 Subject: [PATCH 13/25] Coach type price change on seat selection updates --- .../backoffice/src/app/passengers/page.tsx | 2 +- .../backoffice/src/app/tariff-rates/page.tsx | 5 +- .../backoffice/src/app/tickets/page.tsx | 9 +- .../backoffice/src/lib/api/passengers.ts | 9 +- .../portal/src/app/booking/payment/page.tsx | 8 +- .../portal/src/app/booking/results/page.tsx | 12 +- .../portal/src/app/booking/review/page.tsx | 25 +- .../portal/src/app/booking/seats/page.tsx | 16 +- .../portal/src/app/packages/[id]/page.tsx | 254 ++++++++++-------- .../portal/src/lib/booking-store.ts | 4 +- 10 files changed, 208 insertions(+), 136 deletions(-) diff --git a/apps/edr-passenger-web/backoffice/src/app/passengers/page.tsx b/apps/edr-passenger-web/backoffice/src/app/passengers/page.tsx index cff41ede2..e2c0fefb5 100644 --- a/apps/edr-passenger-web/backoffice/src/app/passengers/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/passengers/page.tsx @@ -140,7 +140,7 @@ export default function PassengersPage() {
), }, - { key: 'phone', label: 'Phone', sortable: true, render: (p: any) => p.phone || 'N/A' }, + { key: 'phone', label: 'Phone', sortable: true, render: (p: any) => p.phone || p.passenger?.user?.phone || '—' }, { key: 'nationality', label: 'Nationality', sortable: true, render: (p: any) => p.nationality || 'N/A' }, { key: 'gender', label: 'Gender', sortable: true, render: (p: any) => p.gender || 'N/A' }, { key: 'dateOfBirth', label: 'Date of Birth', sortable: true, render: (p: any) => p.dateOfBirth ? formatDate(p.dateOfBirth) : 'N/A' }, diff --git a/apps/edr-passenger-web/backoffice/src/app/tariff-rates/page.tsx b/apps/edr-passenger-web/backoffice/src/app/tariff-rates/page.tsx index 33f39d87e..ad10052fd 100644 --- a/apps/edr-passenger-web/backoffice/src/app/tariff-rates/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/tariff-rates/page.tsx @@ -166,7 +166,10 @@ export default function TariffRatesPage() { }, { key: 'coachType', label: 'Coach Type', - render: (c: any) => {c.coachType?.name || c.coachTypeId}, + render: (c: any) => { + const ct = coachTypesArray.find((t: any) => t.id === c.coachTypeId); + return {ct ? `${ct.code} — ${ct.name}` : c.coachTypeId}; + }, }, { key: 'bedPosition', label: 'Bed Position', diff --git a/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx b/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx index e0c7122e3..c737da4de 100644 --- a/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx @@ -343,15 +343,12 @@ export default function TicketsPage() { key: 'contact', label: 'Contact', render: (ticket: any) => { - const phone = ticket.booking?.passenger?.phone || 'N/A'; - const email = ticket.booking?.passenger?.email || 'N/A'; - + const phone = ticket.booking?.contactPhone || ticket.booking?.passenger?.phone || '—'; + const email = ticket.booking?.contactEmail || ticket.booking?.passenger?.email || '—'; return (
{phone}
-
-
{email}
-
+
{email}
); }, diff --git a/apps/edr-passenger-web/backoffice/src/lib/api/passengers.ts b/apps/edr-passenger-web/backoffice/src/lib/api/passengers.ts index f7191e83b..f94e3d05f 100644 --- a/apps/edr-passenger-web/backoffice/src/lib/api/passengers.ts +++ b/apps/edr-passenger-web/backoffice/src/lib/api/passengers.ts @@ -10,7 +10,10 @@ export const passengersApi = { if (filters?.role) params.append('role', filters.role); if (filters?.page) params.append('page', filters.page.toString()); if (filters?.pageSize) params.append('pageSize', filters.pageSize.toString()); - + if ((filters as any)?.gender) params.append('gender', (filters as any).gender); + if ((filters as any)?.nationality) params.append('nationality', (filters as any).nationality); + if ((filters as any)?.dateFrom) params.append('dateFrom', (filters as any).dateFrom); + if ((filters as any)?.dateTo) params.append('dateTo', (filters as any).dateTo); return apiClient.get>(`/passengers?${params.toString()}`); }, @@ -21,4 +24,8 @@ export const passengersApi = { update: (id: string, data: Partial) => { return apiClient.patch(`/passengers/${id}`, data); }, + + delete: (id: string, cascade = false) => { + return apiClient.delete(`/passengers/${id}${cascade ? '?cascade=true' : ''}`); + }, }; diff --git a/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx index ceb54acd9..3e9cecbc7 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx @@ -72,10 +72,10 @@ export default function PaymentPage() { // split equally across both legs. This guarantees leg totals are consistent with the // per-passenger breakdown rows and the overall reviewed total. const outboundBaseFare = isRoundTrip - ? (reviewedPassengerFares ?? []).reduce((sum, f) => sum + (f.isFree ? 0 : Math.round(f.fareMinor / 2)), 0) + ? (reviewedPassengerFares ?? []).reduce((sum, f) => sum + (f.isFree ? 0 : (f.outboundFareMinor ?? Math.round(f.fareMinor / 2))), 0) : 0; const inboundBaseFare = isRoundTrip - ? (reviewedPassengerFares ?? []).reduce((sum, f) => sum + (f.isFree ? 0 : Math.round(f.fareMinor / 2)), 0) + ? (reviewedPassengerFares ?? []).reduce((sum, f) => sum + (f.isFree ? 0 : (f.inboundFareMinor ?? Math.round(f.fareMinor / 2))), 0) : 0; // reviewedPassengerFares / reviewedTotalMinor are the single source of truth for display @@ -307,11 +307,11 @@ export default function PaymentPage() {
Outbound - {formatFare(Math.round((reviewed?.fareMinor ?? 0) / 2), displayCurrency)} + {formatFare(reviewed?.outboundFareMinor ?? Math.round((reviewed?.fareMinor ?? 0) / 2), displayCurrency)}
Return - {formatFare(Math.round((reviewed?.fareMinor ?? 0) / 2), displayCurrency)} + {formatFare(reviewed?.inboundFareMinor ?? Math.round((reviewed?.fareMinor ?? 0) / 2), displayCurrency)}
)} diff --git a/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx index 33609ba0e..fa52aad32 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx @@ -293,7 +293,17 @@ export default function ResultsPage() { // For round trip inbound, proceed with both schedules if (isRoundTrip && !isOutbound) { - setInboundSchedule(scheduleData); + // Mirror the outbound's coachTypes (fares) onto the inbound schedule so the + // return seat selection page shows the same prices as the outbound leg. + const inboundScheduleData = outboundScheduleData + ? { + ...scheduleData, + baseFareAdult: outboundScheduleData.baseFareAdult, + baseFareChild: outboundScheduleData.baseFareChild, + coachTypes: outboundScheduleData.coachTypes, + } + : scheduleData; + setInboundSchedule(inboundScheduleData); setSelectedSchedule(outboundScheduleData); // Set primary as outbound } else { // For one-way diff --git a/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx index cf14cc87c..57b098dc1 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx @@ -445,7 +445,9 @@ export default function ReviewPage() { const fareMinor = isPackageBooking ? (isFreeChild ? 0 : (seatFare ?? pkgFallback)) : (isFreeChild ? 0 : (seatFare ?? line?.fareMinor ?? 0)); - return { fareMinor, isFree: isFreeChild }; + const outboundFareMinor = isRoundTrip ? ((p as any).outboundSeatFareMinor ?? undefined) : undefined; + const inboundFareMinor = isRoundTrip ? ((p as any).inboundSeatFareMinor ?? undefined) : undefined; + return { fareMinor, isFree: isFreeChild, outboundFareMinor, inboundFareMinor }; }); setReviewedTotal(computedTotal, passengerFares); @@ -560,6 +562,14 @@ export default function ReviewPage() { const isFreeChild = isPackageBooking ? isPkgFreeChild(i) : (line?.isFree ?? (isChild(p) && isFirstChild(passengers, i))); + + // Per-leg fares for round trips + const outboundFare: number | null = isRoundTrip + ? (isPackageBooking ? (packageTierPriceMinor ?? null) : ((p as any).outboundSeatFareMinor ?? null)) + : null; + const inboundFare: number | null = isRoundTrip + ? (isPackageBooking ? (packageTierPriceMinor ?? null) : ((p as any).inboundSeatFareMinor ?? null)) + : null; const seatFare = getPassengerSeatFare(p); const passengerTotal = isPackageBooking ? (isFreeChild ? 0 : (seatFare ?? (isChildPassenger ? pkgChildFare : pkgAdultFare))) @@ -582,6 +592,19 @@ export default function ReviewPage() { {formatFare(passengerTotal, displayCurrency)}
+ {/* Round-trip: show outbound + inbound breakdown */} + {isRoundTrip && !isFreeChild && ( +
+
+ ↗ Outbound + {outboundFare != null ? formatFare(outboundFare, displayCurrency) : '—'} +
+
+ ↙ Return + {inboundFare != null ? formatFare(inboundFare, displayCurrency) : '—'} +
+
+ )}
); })} diff --git a/apps/edr-passenger-web/portal/src/app/booking/seats/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/seats/page.tsx index f8fd23609..ccb953267 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/seats/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/seats/page.tsx @@ -451,16 +451,15 @@ export default function SeatsPage() { // the new seat map data has finished loading. const applyCoachTypeSwitch = (coach: any, matchedType: any) => { const newFare = getCoachTypeFare(matchedType.coachTypeId || matchedType.coachId); - const firstClass = matchedType.classes?.[0]; const newCoachTypeId = matchedType.coachTypeId || matchedType.coachId; const updatedSchedule = { ...(currentSchedule as any), selectedCoachTypeId: newCoachTypeId, selectedCoachTypeCode: matchedType.coachTypeCode || coach.type || "", selectedCoachTypeName: matchedType.coachTypeName || coach.coachTypeName || coach.typeName || "", - selectedSeatClass: firstClass?.name || matchedType.coachTypeName || coach.coachTypeName || "", - selectedSeatClassName: firstClass?.name || matchedType.coachTypeName || coach.coachTypeName || "", - seatClassName: firstClass?.name || matchedType.coachTypeName || coach.coachTypeName || "", + selectedSeatClass: matchedType.coachTypeName || coach.coachTypeName || "", + selectedSeatClassName: matchedType.coachTypeName || coach.coachTypeName || "", + seatClassName: matchedType.coachTypeName || coach.coachTypeName || "", baseFareAdult: newFare ?? (currentSchedule as any)?.baseFareAdult, baseFareChild: newFare ?? (currentSchedule as any)?.baseFareChild, }; @@ -921,7 +920,7 @@ export default function SeatsPage() { const positionLabel = newSeat?.bedPosition ? `${newSeat.bedPosition.charAt(0).toUpperCase()}${newSeat.bedPosition.slice(1)} berth` : "This seat"; - const legMultiplier = isRoundTrip ? 2 : 1; + const legMultiplier = isPackageBooking && isRoundTrip ? 2 : 1; setModalState({ isOpen: true, @@ -1328,15 +1327,16 @@ export default function SeatsPage() { useEffect(() => { if (isRoundTrip) { if (!outboundSchedule || !inboundSchedule || !passengers.length) { - router.push("/booking/search"); + router.push(isPackageBooking ? "/booking/passengers" : "/booking/search"); } } else { if (!selectedSchedule || !passengers.length) { - router.push("/booking/search"); + router.push(isPackageBooking ? "/booking/passengers" : "/booking/search"); } } }, [ isRoundTrip, + isPackageBooking, selectedSchedule, outboundSchedule, inboundSchedule, @@ -1712,7 +1712,7 @@ export default function SeatsPage() { if ( isRoundTrip - ? !outboundSchedule || !inboundSchedule || !passengers.length + ? !outboundSchedule || (!isPackageBooking && !inboundSchedule) || !passengers.length : !selectedSchedule || !passengers.length ) return null; diff --git a/apps/edr-passenger-web/portal/src/app/packages/[id]/page.tsx b/apps/edr-passenger-web/portal/src/app/packages/[id]/page.tsx index 32e3f1178..bb839906d 100644 --- a/apps/edr-passenger-web/portal/src/app/packages/[id]/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/packages/[id]/page.tsx @@ -221,7 +221,7 @@ function groupTiersByCoachType(tiers: PriceTier[]): Array<{ if (!map.has(key)) { map.set(key, { coachTypeId: ct?.id ?? key, - coachTypeName: ct?.name ?? tier.seatType, + coachTypeName: ct?.name ?? tier.label ?? tier.seatType, coachTypeCode: ct?.code ?? '', coachTypeType: ct?.type ?? 'passenger', tiers: [], @@ -270,90 +270,121 @@ function PriceTiersPanel({ } return ( -
-

Select Coach Type

- {groups.map((group) => { - const CoachIcon = getCoachIcon(group.coachTypeType); - const allSoldOut = group.tiers.every((t) => t.availableSeats === 0); - const isSelected = selectedId === group.coachTypeId; - return ( -
!allSoldOut && setSelectedId(isSelected ? null : group.coachTypeId)} - > - {/* Coach type header */} -
-
- -
-
-

- {formatCoachTypeLabel(group.coachTypeType)} -

-

- From {formatPrice(group.minPrice * priceMultiplier, group.currency)} - {allSoldOut && · Sold out} -

-
+
+

Choose Coach Type

+
+ {groups.map((group, index) => { + const CoachIcon = getCoachIcon(group.coachTypeType); + const allSoldOut = group.tiers.every((t) => t.availableSeats === 0); + const isSelected = selectedId === group.coachTypeId; + return ( +
!allSoldOut && setSelectedId(isSelected ? null : group.coachTypeId)} + onKeyDown={(e) => { + if (!allSoldOut && (e.key === 'Enter' || e.key === ' ')) { + e.preventDefault(); + setSelectedId(isSelected ? null : group.coachTypeId); + } + }} + className={`group relative w-full p-2 rounded-2xl border-2 text-left transition-all duration-200 ${allSoldOut + ? 'border-gray-200 dark:border-gray-700 opacity-50 cursor-not-allowed' + : isSelected + ? 'border-primary bg-gradient-to-br from-primary/8 to-primary/3 dark:from-primary/15 dark:to-primary/5 shadow-lg shadow-primary/20 scale-[1.02] cursor-pointer' + : 'border-gray-200 dark:border-gray-700 shadow-sm hover:border-primary/40 hover:shadow-md hover:scale-[1.01] bg-white dark:bg-gray-800/50 cursor-pointer' + }`} + style={{ animation: `fade-in-up 0.3s ease-out ${index * 0.08}s both` }} + > + {/* Radio indicator */} {!allSoldOut && ( -
- {isSelected && } -
+ + {isSelected && } + )} -
- {/* All available classes for this coach type */} -
- {group.tiers.map((tier) => { - const soldOut = tier.availableSeats === 0; - return ( -
-
-
-

{tier.seatType.trim()}

-

- {formatPrice(tier.priceMinor * priceMultiplier, tier.currency)} - {soldOut ? ( - Sold out - ) : ( - {tier.availableSeats} left - )} -

+
+
+
+ +
+ +
+

+ {formatCoachTypeLabel(group.coachTypeType)} +

+ {allSoldOut && ( + Sold out + )} +
+ From + + {((group.minPrice * priceMultiplier) / 100).toFixed(2)} + + {group.currency}
- ); - })} -
+
- {/* Book Now — only when this group is selected */} - {isSelected && !allSoldOut && ( -
- + {/* Class options — always visible, matching results page style */} + {group.tiers.length > 0 && ( +
+
+ {group.tiers.map((tier) => { + const soldOut = tier.availableSeats === 0; + return ( +
+ {tier.seatType.trim()} + {soldOut ? ( + Sold out + ) : ( +
+ + {((tier.priceMinor * priceMultiplier) / 100).toFixed(2)} + + {tier.currency} +
+ )} +
+ ); + })} +
+
+ )} + + {!isSelected && !allSoldOut && ( +

+ Click to select this coach +

+ )} + + {isSelected && !allSoldOut && ( + + )}
- )} -
- ); - })} +
+ ); + })} +
+
); } @@ -407,7 +438,7 @@ function PassengerCountModal({

Coach type

-

{tier.seatClass?.coachType?.type ? formatCoachTypeLabel(tier.seatClass.coachType.type) : tier.label.trim()}

+

{tier.seatClass?.coachType?.name ?? tier.label.trim()}

{remaining} seats remaining · from {formatPrice(minPriceMinor * priceMultiplier, tier.currency)} per adult · 1st child per adult free (no seat)

@@ -416,26 +447,26 @@ function PassengerCountModal({ { label: "Adults", sub: `Age 5+ · max ${PKG_MAX_ADULTS}`, value: adultCount, min: 1, max: Math.min(PKG_MAX_ADULTS, remaining), set: (v: number) => { setAdultCount(v); const newMax = Math.min(v * PKG_CHILDREN_PER_ADULT, v + Math.max(0, remaining - v)); setChildCount(c => Math.min(c, newMax)); } }, { label: "Children", sub: `Under 5 · max ${PKG_CHILDREN_PER_ADULT} per adult · 1st per adult FREE (no seat)`, value: childCount, min: 0, max: Math.min(adultCount * PKG_CHILDREN_PER_ADULT, adultCount + Math.max(0, remaining - adultCount)), set: setChildCount }, ].map(({ label, sub, value, min, max, set }) => ( -
-
-

{label}

-

{sub}

-
-
- - {value} - -
+
+
+

{label}

+

{sub}

- ))} +
+ + {value} + +
+
+ ))} {freeChildren > 0 && (
@@ -457,15 +488,14 @@ function PassengerCountModal({ {/* Departure Station */}
-