Table of Contents

Request lifecycle

stateDiagram-v2
    [*] --> Draft: employee starts a request
    Draft --> PendingApproval: Submit (validation + route built)
    Draft --> Completed: Submit when the route this form takes has no step (record only)
    Draft --> InProgress: Submit when nobody approves (no approval step, or a feature approves on submission) and the route has a task
    PendingApproval --> ReturnedForInfo: the system returns an expired hold, or an approval whose party left (ReturnBySystemAsync, no actor)
    PendingApproval --> PendingApproval: a step approved or a task before an approval finished, next step due
    PendingApproval --> ReturnedForInfo: approver returns for info
    ReturnedForInfo --> PendingApproval: Resubmit (route rebuilt from scratch)
    PendingApproval --> Rejected: approver rejects (reason required)
    PendingApproval --> InProgress: last approval approved, a task follows
    PendingApproval --> Completed: last approval approved, no task follows
    InProgress --> InProgress: a task finished, the next one due
    InProgress --> Completed: the last task finished
    Draft --> [*]: owner deletes (hard delete, drafts only)
    PendingApproval --> Cancelled: owner (not after a step with no return) or CancelAnyRequest
    InProgress --> Cancelled
    ReturnedForInfo --> Cancelled
    Completed --> Cancelled: a booking whose time has not passed (StillLiveAsync)

What happens on Submit

Every step below reads def.AtSubmission: the form without the fields a step of the route collects. Those are asked at their step, not here.

  1. Booleans.Fill makes every unticked yes/no "no", so within_working_hours (the requester's own answer, never computed) always has a value for the chain to read. A box a step collects stays unanswered until its step.
  2. Every IRequestListener.PrepareAsync fills the fields its feature owns (DerivedFrom.FromFeature: the room's type from the room) and checks its own rules, before validation so conditions see the values; then HiddenFields.Strip drops the values of fields whose conditions no longer hold.
  3. FormValidation checks required fields, bounds, the service's field groups (FieldCheck: compare, at least one, sum, all or none) — on the server, always.
  4. Required attachments are counted.
  5. ApprovalChain.Steps evaluates each step's conditions against the form and keeps the ones that hold, approvals and tasks, in order (ApprovalChain.Submitted). A listener may approve on submission (ApprovesOnSubmissionAsync: a room type that needs no approval): the approvals are dropped and the tasks kept. A type published with no approval step has nobody to approve it. A type that has approvals, none of which holds for this form, is refused (ApprovalChain.Unresolved, RequestService.NoChainText), unless a listener approved it.
  6. Every approval is resolved to a person by ApproverDirectory: the parties that follow the requester (line manager, branch officer, the requester) must resolve, on a step of either kind, or submission is refused with a clear message; positions may be vacant (they show on the dashboard). Approvals are assigned now. A task held by a party is assigned when it falls due, to the party as it is then; a position's task stays open to every holder of the position. A task with no holder is refused at publishing (SCRUM-146: it was the section's closers').
  7. The previous route (if any) is discarded, the new rows saved, TypeVersion pinned to the published version, the first step's deadline started (Deadlines, see below; none for a request complete on submission), the status set by RequestRoute.Status (PendingApproval while an approval is pending; otherwise InProgress while a task is, its holders told, and Completed when the route is empty), an activity logged, notifications queued, then one SaveChanges.

If anything fails before step 7, nothing is written. Failed submissions do not leave stray drafts.

Deciding a step

  • Only the current step (RequestRoute.Current: the lowest undecided order) can be acted on; later holders cannot skip ahead. What the page and the sheet offer is keyed on its kind (DecisionSheet.ModeFor).
  • CanAct = the step is assigned to me, or it is open to my role. A step with no holder is nobody's.
  • Reject needs a reason and closes the request. Return-for-info sends it back to the requester, who fixes and resubmits — the route is rebuilt because the data changed.
  • No return after a step. A task may carry "قفل الطلب بعد هذه الخطوة" (StepDef.NoReturnAfter, copied onto its row at submission): the car was handed over. Once it is done, RequestRoute.Locked refuses a return (by an approver or by the system), a rejection and the requester's cancellation with one sentence; only CancelAnyRequest still cancels. The page and the sheet hide those buttons by the same rule and say why with that sentence; the page's cancel button asks CancelRefusalAsync, which is CancelAsync's own rule, and shows its reason in the button's place.
  • Every decision bumps ServiceRequest.Version; two approvers racing on the same step cannot both succeed.

Fields collected at a step

A step may collect fields of the form (StepDef.Fields, "حقول الخطوة" in the step dialog of the editor's "المسار" tab): the line manager picks the car while approving, the requester reads the odometer when taking it. The fields stay in def.Fields; the step names their keys, and a field belongs to one step at most. The editor's "الحقول" tab lists each field under the moment it is asked (CatalogDraft.StageOf): at submission, or under the step that collects it, in route order, a step's group headed by the step and not a number (StepWords.Collecting); a row is edited in its FieldDialog, and "+ حقل" in a step's group adds the field to that step.

  • The row knows its step. Each row carries StepIndex, the index of its step in the request's own version, set at submission; RequestRoute.StepOf(def, row) reads it. Rows from before this have none and collect nothing.
  • Two scopes, written once. def.AtSubmission is the requester's form (submission, the new-request page, its review, the requester's own editing, the editor's preview); def.ForStep(step) is the step's.
  • Written by the step's holder, while it is due, through its action. ApproveAsync, AmendAndApproveAsync and CompleteStepAsync take the step's values; RequestService.WriteStepValues refuses a key the step does not collect, fills its unanswered boxes with "no", and validates the step's fields (required, ranges, order). The values go into the step's log line. Return and reject take none. The request page draws the step's fields above the comment and asks the same rule before the confirm; the sheet offers "فتح الطلب" instead of its buttons, and shows the route (RouteTimeline) for a service whose steps collect fields.
  • Never by the requester. A draft save, the resubmission's save and a new draft keep every step key from the stored row; a prefill link fills only the requester's fields. A returned request keeps what its steps collected.
  • On the request. The "البيانات" tab draws the submission's fields only (RequestDataView); the "المسار" tab (RouteTimeline) draws each step in order with what it collected under it, through RequestDataView's step view (Step), so a value is drawn one way whoever asked it.
  • Never deciding what is fixed at submission. Publishing refuses a step's field in a route or duration condition, one that is approver-editable, computed, a computed field's source or a table, one two steps collect, a field shown by a field a later step collects, and what a feature reads as the request is submitted (a booking's room, day, window and attendees: RoomFields.Reads). The step dialog's picker refuses by the same rule.

Photos at a step

A step may collect a photo field (FieldType.Photo, "صور بالكاميرا"): the requester photographs the car at pickup and at return. Its photos are attachments carrying the field's key (Attachment.FieldKey); the field has no value in the form, so FormModel, FormValidation, FieldDisplay and HiddenFields skip it.

  • Taken live, on a phone. The step's form draws PhotoField: the photos added so far, the count against the field's most and its fewest, and, on a device held in the hand (a phone or a tablet), a button that opens PhotoCamera in a window closed by its own buttons only. camera.js (an ES module) opens the back camera with getUserMedia and owns the shutter: the frame is drawn on a canvas in the browser and listed in the window, drawn from its blob with a drop button; nothing reaches the server while shooting or dropping. "إضافة" posts the listed shots in order over HTTP to FileEndpoints.PhotoUpload (the field's key and the shot's id in the query), each with an id made at the shutter, never on the page's circuit; the endpoint refuses a post from another site and saves through AttachmentService.SavePhotoAsync, which keeps a shot once by its id (unique on the request in the database). A refused or lost one stays listed with its line, and the window closes once none is left. A line that drops never locks the window or ends the page: each shot's answer is waited for PhotoCamera.ShotWait at most, the send a little longer since the last answer, and a shot sent again after its answer was lost is answered as kept. A camera that cannot restart as the page comes back says why. Closing over listed shots, or leaving the page, asks first. There is no file input, so nothing comes from the gallery. The stream stops when the window closes and while the page is hidden. A computer says to open the request on a phone.
  • Who and when. Only the holder of the step due now (RequestService.CanAct), while that step collects the field, adds a photo; never past the field's most (Photos.Most). A photo records the route row it was added at (Attachment.StepRowId), and a step counts only its row's (Photos.TakenAt): when a returned request comes back to a photo step, the photos it took stay as history and the holder takes them again. Once added it is final for the holder: only a reader with Permission.DeleteAnyRequest removes one, at any time, from the route, after a confirm. Each add and remove moves the version and writes ActivityKind.PhotoAdded / PhotoRemoved and an audit row naming the field and the step.
  • Finishing the step. WriteStepValues adds Photos.Missing: each photo field shown needs its fewest (Min, and at least one when required, whatever its Min). The page asks it before the confirm; the engine refuses by it in the step's action.
  • Not the request's documents. The 10-file cap, the required attachments and the requester's draft/returned rule count FieldKey == null only; AmendAsync refuses a photo key. AttachmentsPanel lists the request's own files; the route (RouteTimeline, through RequestDataView's step view) shows each step's photos under it, with the server's time (when it was added), opening from /attachments/{id} (RequestAccess.MayView), then those of an earlier round under "صور سابقة".
  • Publishing refuses a photo field no step collects, a condition or a lookup's scope that reads one, a photo column, the approver's correction of one, and counts that are not whole, above 10, or a fewest above the most.
  • What it proves. The photo came through the app's shutter, added by the step's holder while the step was due, at the server's time (the add, not the shutter: a shot waits in the window until "إضافة"). It cannot prove the photo shows the real car: a photo of a screen or of a printed picture, a virtual camera, or a hand-made post of any JPEG to the same address as the shutter's. No browser can prove the lens is real.

Amending

An approver corrects, never re-routes: AmendAndApproveAsync changes the fields the catalog flags ApproverEditable on the current step and approves it in the same write (with the step's own fields, as a plain approval). The administrator (AmendAnyRequest) changes a pending or in-progress request through AmendAsync (how a booking is moved: the listener re-checks the rules and the booking row follows the form), a value a step collected included, never one a step has yet to ask. Both refuse a change that would alter the chain or a step's deadline rule, judge the new data on the form asked so far (RequestRoute.Asked), write the diff and the reason to the log (ActivityKind.Amended) and tell the requester.

The system itself returns a pending request with no actor when a held room slot's start passes unapproved (RoomHoldExpiry through ReturnBySystemAsync): the same state, log row and notification as a return by an approver.

Tasks and closing

A service has one route, RequestTypeDef.Steps (stored under the JSON name Approvals, the "المسار" tab of the catalog editor: a spine of cards, each step in words from StepWords, edited in its StepDialog): approvals and tasks in order, each with its holder and its conditions. A step has no number in the editor: a conditional one (a hollow dot, a dashed card, "شرطي") is on a request's route only when its conditions hold, so its place differs from one request to another. "معاينة المسار" asks for the fields the conditions read and numbers the route a request with those values takes, through ApprovalChain.Indexed on the draft's definition (an unanswered yes/no filled as at submission); it never writes the draft. A request's own route is fixed at submission, and its page numbers it. A task may sit anywhere, between two approvals included: the request waits for approval meanwhile, and the next approver sees nothing, in the inbox or the bell, until the task is done. A task is held by a position, by a party that follows the requester (the requester themselves, the line manager, the branch officer); publishing refuses a task with no holder, and the requester never holds an approval. The section's closers («موظفو قسم …», the holders of the section's closing permission) were folded once into a position per section with the same people (SCRUM-146, RequestsSeed.FoldSectionStaffAsync). The questions are written once on the definition: Approvers, Tasks, CompletesOnSubmission (no step at all).

  • The rows. At submission the engine copies the route of the request's own version into RequestApprovals rows, each with its Kind and, for a task, its NameAr; resubmission discards them all. RequestProgress draws the rows; a request routed before tasks were rows has its version's tasks drawn after its approvals.
  • Who may act. RequestService.CanAct, for both kinds: an approval's assignee while they hold the position, else any holder of its role; a party's task is its assignee's (the requester's own task is simply assigned to the requester); a task with a position is its holders', and nobody else's; a row with no holder is nobody's. Inbox.PendingQuery (approvals, waiting for approval) and Inbox.ExecutionQuery (tasks, in either status, "assigned to me" included) are the same rule in SQL over the step due now, and RequestAccess.MayView admits whoever may act on a row the route has reached, and whoever was told one of its steps is late. The page, the sheet and the attachments ask RequestAccess.MayViewAsync, which reads who was told.
  • A party's task falls due. The one advance routine (TaskFallsDueAsync) resolves the party at that moment: the key goes back to the current line manager. Nobody there (the manager left meanwhile), the row stays unassigned, a Note in the log says so, and the dashboard lists it as stuck ("لا يوجد مكلَّف الآن"). The stalled sweep (StalledStepSweep, ReturnStalledAsync) hands it to the party once one is set, or to the new holder when its holder is deactivated: a write with the version, a log line, the audit row request.step_assigned and a notice. It never returns such a request; a party's approval whose approver left goes back to its requester as before, unless a step with no return is done (then it is handed over too).
  • Finishing a task. RequestService.CompleteStepAsync is the one way through a task: it checks the status, the version the reader saw and CanAct, marks the row done and increments ServiceRequest.Version. A middle step logs ActivityKind.StepDone and tells the holders of the next; the last one closes the request.
  • One closing routine. The last task, the last approval of a route with no task after it, and a submission with no step at all end in the same routine: Completed, ClosedAt, the closure note, the Closed activity, and the requester told (unless they are the one who acted).
  • A position nobody holds. Publishing refuses a task position that is missing or deactivated. A step whose position loses its last holder later is flagged in the dashboard's "where work waits" list; giving the position to an employee unblocks it. There is no silent fallback to anyone else.
  • Nobody can take it. The dashboard flags every step due now whose audience is empty (ApproverDirectory.AudienceAsync, which also picks who is told about a step): a position nobody holds, a party's step with nobody active behind it, and a task with no position whose section has no active closer. A task that falls due to nobody, whoever holds it, writes the same Note and request.step_unassigned row. Each line also shows its late requests under its count, and the "متأخرة" figure is their sum.
  • Bookings are confirmed, not executed: the seeded booking service has no task, so a booking completes at its approval (or at submission when the room needs none). IRequestListener.StillLiveAsync keeps a completed booking cancellable and movable until its time passes. A booking is approved one slot before its start at the latest (IRequestListener.ApproveByAsync, see Deadlines).
  • The fold. Before the route, what followed the last approval was a separate execution block and table. Every stored version and every row was folded into the route once (RouteFold, RequestsSeed.FoldStoredRoutesAsync, the migration TasksJoinTheRoute), with the same steps; a start refuses a version that still carries the block.

Deadlines and lateness

  • A deadline is on a step. StepDef.Due holds rules of hours, days (the real clock) or working days (the "أيام العمل" calendar), each under conditions; the first that holds on the request's form wins (Deadlines.Rule), and publishing refuses a last rule with a condition, a rule with none before the last (it hides the rest), an amount outside 1 to 999 (0 working days is the end of the same working day), a rule on an external service and a condition on a field a step collects.
  • Counted from when the step becomes due. One routine, RequestService.StartDeadlineAsync, sets the row's RequestApproval.DueAt when the row becomes the step due now: the submission's first step and every advance. A resubmission rebuilds the rows, and so their deadlines. Working days from a day off (a weekend, a holiday) count the next working day whole and end at a working day's end (WorkCalendar.AddWorkingDays), so steps that fall due over one weekend share their deadline. A restore gives the step due now back the time the request lay deleted.
  • A feature may need the approval sooner. IRequestListener.ApproveByAsync reads from the form the latest the request should be approved (a booking: one slot of the policy before its start, so the late pass tells its approver before the hold is returned at the start). While the request waits for approval, the step's deadline is the earlier of its rule and that time; a step with no rule gets none from it. The review, the sent screen, the track and the route show the time beside each step ahead it caps (ArabicNames.CappedBy, one rule).
  • Said one way. The step due now's deadline is the step's ("مهلة الخطوة"), with its hour while near and a working day's end as the day's end, on every screen that shows it (ArabicNames.StepDue, named "المهلة حتى …" where no label says what it is), and nothing is said for a step with none; a step ahead says how long it will have once it starts (ArabicNames.Ahead); the launcher's card and the new-request page name the step its deadline is on (ArabicNames.ServiceDeadline). An administrator's correction that moves that time (a booking moved) re-caps the step due now and never restarts its rule: sooner, the new time caps it; later, a deadline the old time capped goes back to the rule counted from when the step fell due. A step still late keeps its late notice, so it is told once.
  • Late is one rule. RequestRoute.IsLate(req, now): the step due now passed its deadline while the request waits for approval or is in progress. RequestRoute.Late(now) is the same rule in SQL, for the "المتأخرة فقط" filter, the home page's count and the sweep (the dashboard asks IsLate of the rows it loads); RequestRoute.DueFirst orders the inbox. A list loads the route with its rows so each row can say it.
  • When a step passes its deadline, the stalled sweep's second pass (TellLateAsync) tells its holders and each holder's line manager once (NotificationKind.StepLate), writes a Note and the audit row request.step_late, and marks the row (LateNotifiedAt). It does not increment the version: a holder deciding at that moment is not refused. Whoever it told may open the request; a deactivated holder is nobody, so such a step waits untold and flagged. A step handed to a new holder is told again, to the new holder.
  • The move. The request-level SLA of every stored version moved once onto the step that ends its route (DeadlineFold, RequestsSeed.FoldStoredDeadlinesAsync), and each request on its way kept its due date on its step due now (the StepDeadlines migration, DeadlineRows.Move).

Deleting

Drafts are hard-deleted by their owner. Anything that entered the route is soft-deleted (DeletedAt, DeletedByUserId, reason) by an admin and can be restored; a global query filter hides it everywhere. A feature may refuse a restore (IRequestListener.RestoreRefusedAsync): a booking waiting for approval past a step with no return, whose slot another request took meanwhile, stays deleted while the slot is taken, and the refusal names that request. A restored request's step due now gets back the time it lay deleted, under the time a feature needs it approved by, and a later correction that counts the step's rule again counts that time too.

Where the employee does each step

  • Submit — NewRequest.razor in three steps: fill (DynamicForm), review (ReviewStep: the data, the route the request will take with each step's deadline in words), sent (DoneStep: the number, the next step and its deadline, the progress track). Validation runs before the review, so an incomplete form never creates a draft. A saved draft or a returned request goes out from its own page through the same review ("متابعة", then "تقديم الطلب" or "إعادة التقديم"): both pages hand ReviewStep the form as it will be submitted, and it draws the data and the route from that form alone.
  • Approve / return / reject / finish a step — from the task inbox or Home through DecisionSheet, a sheet with the facts, the progress track, the data and a comment; or from the full request page, whose "دورك الآن" card (DecisionPanel) is shown to the holder of the step due now alone: the step's name and deadline, its fields and photos, the note and the buttons. Under it, three tabs: "البيانات" (what the requester submitted), "المسار" (RouteTimeline: each step's holder, who acted and when, the decision and comment, what it collected; the step due now marked, the steps ahead greyed with their holder and deadline; then "مكتمل" with the service's closing description, muted, while the request is open or complete: a rejected or cancelled one has no end) and "السجل", where the last step's note reads "ملاحظة الخطوة الأخيرة". A step is named one way on the page (StepWords.Named: a task by its name, an approval as "اعتماد" and its holder), in the header's "الخطوة الحالية", on the card and on the route. Every task is finished with "تم", the last or only one included: its confirm says the request completes with it ("إتمام الخطوة الأخيرة"); the words and the call are one definition in Decisions. A task row names the step due now, whatever its name. The requester's own task shows on their request page through the same panel, and in their task inbox and Home through the same lists.
  • Follow — RequestProgress draws the same track everywhere: under the request title, in the sheet and on the sent screen. It takes the request and its own version's definition; a long track scrolls inside itself.