Skip to content

BUSINESS PROCEDURES

VietGara — Multi-Tenant SaaS Platform for Auto Garage Management & Maintenance

Version 1.1 — October 16, 2026

1. Purpose and how to read it

The Functional Requirements say what the platform does; the Low-Level Design says how it is built. This document sits between them: for each procedure that involves more than one person or system, it shows who does what, in which order, which rules apply and what happens when something goes wrong. Support, QA and the people who run the platform read it to understand a case; it also fixes the wording the screens and e-mails use.

Each procedure names its actors, its trigger, a diagram, the rules, the exceptions, the notifications and the requirements, API calls and test scenarios it belongs to. The technical sequence of Quote → Repair → Settlement is in the High-Level Design (Section 4.1); P9 describes it from the accountant’s side. P10 to P13 describe the part catalogue and the stock; each marks what works today (up to Release 6.0) and what arrives with the inventory documents of Phase 7.

#ProcedureActorsRequirements
P1Registration, e-mail verification and trialOwner, systemFR-IAM-01, FR-LIC-01
P2Subscription lifecycle: expiry, grace period and suspensionSystem, ownerFR-LIC-04
P3Changing plan or renewing by bank transferOwner, platform adminFR-LIC-02, FR-LIC-07, FR-LIC-08
P4Account credit and refundsOwner, platform adminFR-LIC-09
P5Platform payment detailsPlatform adminFR-LIC-10
P6Staff invitationOwner or manager, inviteeFR-TEN-04
P7Creating a garage and the plan limitsOwnerFR-TEN-02, FR-LIC-02
P8Linking a customer to a userService advisor, user (end customer)FR-CUS-06, FR-CAPP-05
P9Settling a repair order and collecting paymentsAccountant, systemFR-BIL-01, FR-BIL-03, FR-BIL-05, FR-BIL-06
P10Setting up the part catalogue: units, packaging, categories, price, imagesInventory staff, ownerFR-CAT-01 to FR-CAT-04, FR-CAT-07, FR-CAT-09
P11Receiving stock (stock-in)Inventory staff, supplier, systemFR-INV-01, FR-INV-03, FR-CAT-02; Phase 7: FR-INV-06, FR-INV-07, FR-INV-19
P12Issuing parts to a repair orderSystem, inventory staff, service advisorFR-INV-02, FR-INV-03, FR-CAT-02; Phase 7: FR-INV-15, FR-INV-20
P13Counting stock and correcting it (Release 7.1)Inventory staff, ownerFR-INV-09 to FR-INV-12
P15Quoting with jobs, and changing, waiting and cancelling an order (Release 7.3)Service advisor, owner, inventory staffFR-CAT-05, FR-CAT-06, FR-CAT-08, FR-INV-15, FR-REP-07, FR-REP-08

2. Terms used

TermMeaning
OwnerThe user who owns the subscription; one subscription covers all the owner’s garages
Platform adminA user listed in PLATFORM_ADMIN_EMAILS; works in vietgara-admin-web or vietgara-admin-app
PeriodThe time a plan is paid for, one billing cycle; it has a start and an end
RequestA plan payment request: the owner’s quote, transfer declaration and the admin’s decision, in one record
ReferenceThe unique code of a request (for example VG-7K2M9Q) that the owner writes in the transfer note
CreditThe owner’s account credit, in VND, applied automatically to the next payment
Grace periodThe 7 days after a period ends in which the garages are read-only
PartAn item with a SKU, in the Catalogue module (parts); Inventory keeps its stock
Base unitThe unit a part’s stock is counted and stored in (for example the can)
Packaging unitAnother unit of the same part with a fixed factor to the base unit (a case of 24 cans)
Entered quantityWhat the user typed, in the unit chosen; the system stores it next to the base-unit quantity
On hand / reserved / availableStock in the base unit; the part of it promised to draft stock issues (Phase 7); on hand minus reserved

3. P1 — Registration, e-mail verification and trial

Actors: owner, system. Trigger: a person signs up in the web console or an app.

flowchart TD
  A([Owner signs up]) --> B{How?}
  B -- E-mail and password --> C[E-mail an OTP, valid 2 minutes]
  C --> D{OTP correct within 5 attempts?}
  D -- no, resend after 30 s --> C
  D -- yes --> E[E-mail verified]
  B -- Google --> E
  E --> F[user.email_verified]
  F --> G[Default roles are created]
  F --> H[14-day trial subscription starts]
  F --> I[Registration e-mail is sent]
  H --> J([Owner creates the first garage])

Rules

  • Until the e-mail is verified the user can only edit their profile and verify; garages, staff and business pages are blocked.
  • A Google account is verified on sign-up, so the same event follows at once.
  • The trial gives every feature and the limits of the trial plan for 14 days. It is given once, by the system; the trial plan can never be chosen again.
  • A user without a subscription (an owner whose verification event was lost) gets the trial the first time the subscription is read; the creation is idempotent.

4. P2 — Subscription lifecycle: expiry, grace period and suspension

Actors: system (the expiry job), owner. Trigger: the clock; the job runs every LICENSE_EXPIRY_JOB_INTERVAL (default 1 hour).

stateDiagram-v2
  [*] --> trial: e-mail verified
  trial --> active: payment approved
  trial --> past_due: trial ends
  active --> past_due: period ends without a payment
  past_due --> active: payment approved
  past_due --> suspended: 7 days of grace end
  suspended --> active: payment approved
sequenceDiagram
  actor O as Owner
  participant J as Expiry job
  participant N as Notification
  J->>N: 7 days before the end: subscription.expiring_soon
  N->>O: e-mail with the amount to renew and the bank details
  O->>O: renews (P3)
  Note over J: if the period ends without a payment
  J->>J: status past_due, grace period starts (garages are read-only)
  J->>N: subscription.status_changed
  N->>O: e-mail: read-only until the end of the grace period, how to pay
  Note over J: 7 days later
  J->>J: status suspended (garages are locked)
  J->>N: subscription.status_changed
  N->>O: e-mail: suspended, how to pay
StatusWhat the owner can do
trial, activeEverything the plan includes, within its limits
past_due (grace)Read everything, create nothing: the garages are read-only. Paying is allowed
suspendedSign in, see the subscription and pay; every request to a garage is refused until a payment is approved

Rules

  • The reminder is sent once per period, 7 days before the end, with the amount to renew and the platform’s payment details.
  • The job never changes a plan; only an approved payment (P3) or a manual extension by a platform admin does, and either reactivates a past_due or suspended subscription.
  • Each status change is published as subscription.status_changed and the entitlement cache is refreshed; the console shows the status and what to do.
  • The owner is e-mailed when the subscription becomes past_due and when it becomes suspended, each time with the amount to pay and the platform’s bank details; a lapsed trial is worded as such. Reactivation is not e-mailed by this event: an approved payment has its own e-mail (P3).

5. P3 — Changing plan or renewing by bank transfer

Actors: owner, platform admin, system. Trigger: the owner chooses another plan, or renews the current one, in the console or the owner app. Until online payment arrives (Phase 16) this is the only way to pay.

sequenceDiagram
  actor O as Owner
  participant L as License
  participant N as Notification
  actor A as Platform admin
  O->>L: choose a plan (change) or renew
  L->>L: check the plan and the usage limits
  L-->>O: quote: amount due, bank details, QR, reference
  O->>O: transfer the amount with the reference in the note
  O->>L: submit: payer, amount, bank reference, date, screenshot
  L->>L: limits again, flag a mismatch with the quote
  L-)N: payment_submitted
  N->>A: e-mail: a request waits
  A->>L: open the review queue and the evidence
  alt the money is received
    A->>L: approve (a note is required for a mismatch)
    L->>L: limits again, plan applies, new period, credit, entitlements
    L-)N: payment_approved
    N->>O: e-mail: the plan is active
  else it is not
    A->>L: reject with a reason
    L-)N: payment_rejected
    N->>O: e-mail with the reason
  end
stateDiagram-v2
  [*] --> awaiting_payment: owner starts a request
  awaiting_payment --> awaiting_payment: replaced by a new request
  awaiting_payment --> submitted: owner submits the transfer
  awaiting_payment --> cancelled: owner cancels
  submitted --> cancelled: owner cancels before review
  submitted --> approved: admin approves
  submitted --> rejected: admin rejects

What the owner sees and does

  1. Opens Subscription, chooses a plan (or Renew). The quote shows the new price, the credit applied and the amount due, with the platform’s bank details, QR code and the reference.
  2. Transfers exactly the amount due and writes the reference in the transfer note.
  3. Returns to the request, enters the payer’s name, bank and account, the amount, the bank’s transaction code and the date, and attaches a screenshot of the transfer. When the credit covers the whole price the amount due is 0 and no transfer or screenshot is needed.
  4. Waits. The request shows its status; the owner is e-mailed when it is approved or rejected. A rejected owner starts a new request.

What the platform admin does

  1. Opens the review queue (oldest first) or follows the reminder e-mail.
  2. Compares the request with the bank statement: reference, amount, payer and date, using the screenshot.
  3. Approves, or rejects with a reason. When the amount transferred differs from the quote, a warning shows; the admin contacts the owner, and may approve with a note that records the outcome.

Rules

RuleDetail
One open requestAn owner has one request in awaiting_payment or submitted. Starting another replaces one that still waits for payment; a submitted one must be cancelled or reviewed first (REQUEST_ALREADY_OPEN)
PlansAny public, active plan other than the trial plan and the current plan; a renewal pays for the current plan again. A plan that is no longer offered but still active can be renewed by the owners on it
LimitsGarages and staff in use (members plus pending invitations) must fit the new plan, checked at submission and at approval; otherwise PLAN_LIMIT_REACHED lists what to reduce
Immediate effectApproval applies the plan at once and starts a new period of its billing cycle. It does not wait for the end of the current period
RenewalA renewal of the current plan before its end extends from the old end date, so no paid day is lost; after the end it restarts from approval and reactivates the subscription
TrialThe unused trial days are lost when the first payment is approved
Never expiresA submitted request has no deadline: the money has moved, so it waits for the admin however long it takes
QuoteFixed when the request starts and honoured at approval, even if the plan’s price changes meanwhile; the bank details shown are kept with it too
EvidencePNG, JPEG, WebP or PDF, up to 5 MB, private; shown to admins by short-lived signed link
ReminderWhile requests are submitted, the admins are e-mailed once a day
AuditSubmission, approval, rejection and cancellation record who and when, with the values before and after

Amount due

creditFromPeriod = floor(unusedDays / periodDays × price of the current period) // 0 for a trial and for a renewal
available = creditFromPeriod + account credit
creditApplied = min(available, new price)
amountDue = new price − creditApplied
surplus = available − creditApplied // becomes account credit
CaseCurrent periodAccount creditNew planResult
Upgrade after 10 of 30 days600,00001,490,000credit 400,000; amount due 1,090,000
Downgrade after 15 of 30 days600,00050,000100,000available 350,000; amount due 0; surplus 250,000 becomes credit
First payment from the trial0 (trial)0599,000amount due 599,000; trial days lost
Renewal 5 days before the end600,000100,000600,000 (same)no unused credit; amount due 500,000; the period extends from its old end

Exceptions

SituationWhat happens
The owner transfers a different amountThe request is flagged; the admin resolves it with the owner and approves with a note, or rejects; the owner may then submit again
The owner transfers but never submitsThe request stays awaiting_payment; the owner can submit later while it is open. Starting a new one replaces it and the reference changes, so the owner must use the latest
The usage grew after the quoteApproval is refused with PLAN_LIMIT_REACHED; the admin asks the owner to reduce usage or to choose another plan (reject, then a new request)
The admin rejects by mistakeThe owner submits a new request; the earlier one stays in the history
Nobody reviewsThe daily reminder continues; the subscription’s own expiry (P2) is not paused, but an approval reactivates it at any time

Requirements: FR-LIC-02, FR-LIC-07, FR-LIC-08, FR-LIC-09, FR-NOT-04. API: API Specification Section 4 (/me/subscription/payment-requests, /admin/plan-payment-requests). Design: Low-Level Design Section 3.3; Database Design Section 6.8. Scenarios: E2E-LIC (License & Subscription).

6. P4 — Account credit and refunds

Actors: owner, platform admin.

flowchart LR
  A[Plan change before the period ends] --> B[Unused value of the period]
  B --> C{More than the new price?}
  C -- yes --> D[Surplus becomes account credit]
  C -- no --> E[Reduces the amount due]
  D --> F[Applied automatically to the next payment]
  F --> G[Ledger entry: applied]
  H[Admin correction] --> I[Ledger entry: adjustment, note required]

Rules

  • Credit is in VND, never negative and visible to the owner with its history (earned, applied, adjustment).
  • It is applied to every next payment before any transfer is asked for.
  • Refunds are paid manually by the platform outside the system; the admin then records an adjustment with a note that points at the transfer. There is no automatic refund.
  • A manual correction is an audited, signed amount with a mandatory note; a correction that would take the balance below 0 stops at 0.

7. P5 — Platform payment details

Actors: platform admin. Trigger: the bank account changes, or a first setup.

  1. The admin opens Payment details and edits the bank, account number, account holder, branch and transfer instructions, and uploads the QR code image (PNG, JPEG or WebP, up to 1 MB).
  2. The change is audited with who and when, and the values before and after.
  3. New requests and the 7-day renewal e-mails show the new details at once. A request keeps the details that were shown to its owner when it started, so a review always compares with what the owner actually saw.

8. P6 — Staff invitation

Actors: owner or manager (staff.write), invitee, system. Trigger: the owner wants a person to work in a garage. Only existing users can be invited; a person without a user registers first (P1).

sequenceDiagram
  actor O as Owner or manager
  participant T as Tenant
  participant L as License
  participant M as Mail
  actor S as Invitee
  O->>T: look up by e-mail or phone
  T-->>O: candidate: can be invited, already a member, or already invited
  O->>T: invite with the staff groups
  T->>L: staff limit: members plus pending invitations
  T->>M: invitation e-mail in the invitee's language
  alt accepts
    S->>T: accept
    T->>T: membership and groups are created
  else declines
    S->>T: decline
  else the owner withdraws
    O->>T: revoke
  else 7 days pass
    T->>T: expired
  end
stateDiagram-v2
  [*] --> pending: invited
  pending --> accepted: invitee accepts
  pending --> declined: invitee declines
  pending --> revoked: owner revokes
  pending --> expired: 7 days

Rules

  • One pending invitation per garage and user; inviting a current member or an already invited user is refused (ALREADY_MEMBER, ALREADY_INVITED).
  • A pending invitation counts against the plan’s staff limit as if the person had joined, so the limit cannot be exceeded by accepting later (PLAN_LIMIT_REACHED).
  • Only the invitee can accept or decline; another user opening the link is refused. An expired or reviewed invitation cannot be accepted (INVITATION_EXPIRED, INVITATION_NOT_PENDING).
  • Accepting joins the garage with the invited groups; permissions come from the groups’ roles. Staff can then be set Inactive or removed (the person keeps their user).
  • The invitee’s e-mail is in their own language (FR-IAM-07).

9. P7 — Creating a garage and the plan limits

Actors: owner. Trigger: the owner adds a garage (branch).

flowchart TD
  A([Owner creates a garage]) --> B{E-mail verified?}
  B -- no --> X1[Refused: verify first]
  B -- yes --> C{Subscription allows writing?}
  C -- grace or suspended --> X2[Refused: read-only or locked, pay first - P3]
  C -- yes --> D{Active garages below the plan limit?}
  D -- no --> X3[PLAN_LIMIT_REACHED: choose a larger plan - P3]
  D -- yes --> E[Garage and default staff groups created]
  E --> F[garage.created]

Rules

  • The limit counts active garages across all the owner’s garages; setting a garage Inactive frees a place.
  • Every garage under one user shares the one subscription (a branch model); staff and daily transactions are also counted per owner.
  • Exceeding the limit is solved by changing plan (P3); paying per extra garage is planned for Phase 16 (FR-LIC-06).

10. P8 — Linking a customer to a user

Actors: service advisor (customer.write), the person as a user in the Customer App, system. Trigger: a garage wants its customer profile tied to the person’s VietGara user, so the person sees their vehicles and quotes. A customer needs no user: a walk-in profile is complete without a link.

sequenceDiagram
  actor SA as Service advisor
  participant C as Customer and Vehicle
  participant I as Identity
  participant M as Mail
  actor U as User (Customer App)
  SA->>C: send a link request for a customer with an e-mail
  C->>I: does a verified user own this e-mail?
  I-->>C: yes, no, or no answer revealed
  alt no verified owner
    C-->>SA: refused: the garage never learns who the user is
  else a user owns it
    C->>M: one-time code to the user, valid 2 minutes
    C-->>SA: request sent (status requested)
    alt the user reads the code to the advisor
      SA->>C: enter the code
      C->>C: linked through OTP
    else the user answers in the app
      U->>C: accept
      C->>C: linked through APP
    else the user declines
      U->>C: decline
      C->>C: declined
    end
  end
stateDiagram-v2
  [*] --> requested: the garage sends a request
  requested --> linked: code entered, or the user accepts
  requested --> declined: the user declines
  requested --> unlinked: the garage cancels, or the e-mail changed hands
  linked --> unlinked: the garage or the user ends the link
  [*] --> linked: sign-in match by verified e-mail (auto)

Rules

  • Only a verified e-mail can be matched. The garage learns only that a request was sent, never who the user is; a request for an e-mail nobody owns is refused.
  • A customer has at most one current link (requested or linked). Declined and ended links stay as history.
  • Automatic link at sign-in: a user’s verified e-mail links matching profiles when the user signs in, but only when a profile has never had a link with that user (a refusal or an unlink is respected), no other user is linked to it, and no request is still waiting (that one is answered in the app).
  • Ending a link, from either side, keeps the customer profile and its vehicles and documents.
  • If the person has no user or declines, staff keep or create a walk-in customer without one.
  • Phone matching waits for verified phones (Release 2.4); until then a request matches by e-mail only.
  • Link changes are audited (customer.link_requested, customer.link_confirmed, customer.link_answered, customer.link_ended) without naming the user.

Exceptions

SituationWhat happens
The customer has no e-mail, or no verified user owns itThe request is refused with FAILED_PRECONDITION; the advisor keeps the walk-in profile
The code expires or is wrongOTP_EXPIRED / OTP_INVALID; the advisor sends the request again (the code is re-sent, at most once every 30 seconds)
The customer is already linked409 ALREADY_EXISTS
A request waits for one user and the e-mail now belongs to anotherThe old request ends and a new one starts for the new owner

Requirements: FR-CUS-06, FR-CAPP-05. API: API Specification Sections 5 and 14.2. Design: Database Design Section 3.4; Event Design (audit actions).

11. P9 — Settling a repair order and collecting payments

Actors: accountant (billing.write), system. Trigger: a repair order is closed (every line completed).

flowchart TD
  A([Repair order closed]) --> B[Create the settlement]
  B --> C{Billing party}
  C -- default or chosen --> D[Settlement pending: total, billing party, garage legal identity as a snapshot]
  D --> E{VAT invoice requested?}
  E -- party incomplete --> X[INVOICE_DATA_INCOMPLETE: complete the tax profile or bill as retail]
  E -- yes, complete or no --> F[Record a payment]
  F --> G[Payment voucher issued and numbered]
  G --> H{Paid in full?}
  H -- no --> I[Status partially_paid]
  I --> F
  H -- yes --> J[Status paid]
  G --> K[Voucher PDF printed]
  G --> V{Recorded by mistake?}
  V -- yes --> W[Void with a reason: totals recalculated, voucher stays marked void]
  W --> F

Rules

  • A settlement is created from a closed repair order, once (ALREADY_EXISTS if it exists), and counts against the plan’s daily transaction limit. It copies the garage’s legal identity; later edits never rewrite it.
  • Creating a settlement and recording a payment both need an Idempotency-Key; a retry is answered from the first attempt and never recorded twice. A key reused for another request is refused (IDEMPOTENCY_KEY_REUSED).
  • The billing party defaults to the repair order’s (the quote’s), else the customer’s default tax profile, else retail. It can be changed on the settlement together with invoice requested; a VAT invoice needs a legal name, tax ID and address.
  • A payment is the amount actually collected, positive and at most what is still due, with its method. Each payment gets its own voucher (phiếu thu) numbered from the garage’s template, with the payer, reason, amount in figures and words, collector and signature blocks.
  • Several payments may collect the total. The status is derived: pending, partially_paid, paid; a zero-total settlement is paid at once.
  • A wrong payment is voided with a reason (up to 500 characters); the voucher stays on file marked void, the totals and status are recalculated and the audit log records who did it. The correct payment is then recorded again.
  • Online payment and e-invoice issuing arrive in Phase 16; until then the garage collects at the counter or by transfer and records the payment.

Exceptions

SituationWhat happens
The repair order is not closedFAILED_PRECONDITION; close it first
The connection drops after pressing recordRetrying with the same key returns the first answer; nothing is recorded twice
The amount exceeds what is dueVALIDATION_ERROR
A voucher is voided twice409 ALREADY_EXISTS
The subscription is read-onlyRecording is refused until it is renewed (P2, P3)

Requirements: FR-BIL-01, FR-BIL-03, FR-BIL-05, FR-BIL-06, FR-CUS-07, FR-DOC-01. API: API Specification Sections 1.3, 9 and 10. Design: High-Level Design Section 4.1; Low-Level Design Section 4.1. Scenarios: E2E-BIL.

12. P10 — Setting up the part catalogue

Actors: inventory staff or owner (catalogue.write). Trigger: a new part, a new way of packaging it, or a price change. The part, its price, units, packaging, categories and images all belong to the Catalogue module; only the stock of the part belongs to Inventory (HLD Section 3.1). The steps below are the same for the user.

flowchart TD
  A([Need to stock a new part]) --> B{Unit exists?}
  B -- no --> C[Create the unit once per garage: can, case, litre]
  B -- yes --> D
  C --> D[Create the part: SKU, name, base unit, selling price, category, brand, reorder threshold]
  D --> E{Sold or bought in packs?}
  E -- yes --> F[Add packaging units with a factor: 1 case = 24 cans, optional own price]
  E -- no --> G
  F --> G[Attach up to three images]
  G --> H([Part ready to receive, quote and issue])
  H --> I{Later change?}
  I -- price --> J[New price applies to new quotes only]
  I -- not used any more --> K[Delete or deactivate]

Rules

  • Units are per garage. A unit has a name and an abbreviation; names are unique per garage, ignoring letter case. The factor is not on the unit but on the part: a case of beer is 24 cans, a case of filters is 10 pieces.
  • Base unit. Every part has exactly one, chosen when the part is created. All stock, thresholds, and quantities on quotes and repair orders are kept in it. It cannot be changed once the part has stock movements, packaging units or document lines; create a new part instead, because every stored number would silently change meaning.
  • Packaging units. A part may have any number of packaging units, each with a factor above zero (decimals allowed) and optionally its own selling price. The base unit is never one of them, a unit appears once, and an inactive unit cannot be used. The table is saved as a whole.
  • Selling price is what the customer pays per base unit, in whole dong. A packaging unit costs base price × factor unless it has its own price: 9,000 per can makes a case 216,000, or 200,000 when overridden. A quote line prefilled from the part keeps its own copy of the price, so changing the price never changes an existing quote. Everyone with catalogue.read sees the price (all default roles hold it); cost is separate and needs inventory.cost.read (Phase 7), which also adds a warning when a price is below the average cost.
  • Categories form a tree of at most three levels (Engine oil > Synthetic > 5W-30). Names are unique among siblings. A part has at most one category and one brand (free text); lists and pickers filter by SKU, name, category (including its sub-categories) and brand.
  • Images. One to three per part (PNG, JPEG or WebP), uploaded first and then attached; the first is the thumbnail. Images are public files (like the logo), resized on upload.
  • Delete or deactivate (FR-CAT-07). The dialog says which one will happen. A unit, category or part that a document, stock movement or part refers to is deactivated: it disappears from pickers, stays on what already uses it and can be reactivated. One used nowhere is deleted. A category with sub-categories is never deleted from under them.

Exceptions

SituationWhat happens
The same SKU twice in a garage409 ALREADY_EXISTS
No base unit givenVALIDATION_ERROR
A packaging unit equal to the base unit, repeated, inactive or with factor 0VALIDATION_ERROR; nothing is saved
A fourth level of category, or a fourth imageVALIDATION_ERROR
Changing the base unit of a part with stockRefused (FAILED_PRECONDITION)
Deleting a unit that parts useThe unit is deactivated instead; the answer says so
The subscription is read-onlyThe catalogue is read-only too (P2)

Requirements: FR-CAT-01 to FR-CAT-04, FR-CAT-07, FR-CAT-09. API: API Specification Sections 1.5 and 8. Design: Database Design Section 5. Scenarios: E2E-INV, E2E-CAT (to be written with Release 6.0).

13. P11 — Receiving stock (stock-in)

Actors: inventory staff (inventory.write), the supplier, the system. Trigger: a delivery of parts arrives.

Today (up to Release 6.x): one part per receipt

flowchart TD
  A([Delivery arrives]) --> B[Check the goods against the delivery note]
  B --> C[Open the part and choose Receive stock]
  C --> D{Counted in packs?}
  D -- yes --> E[Pick the packaging unit, type the number of packs]
  D -- no --> F[Type the quantity in the base unit]
  E --> G[Optional: unit cost]
  F --> G
  G --> H[Save]
  H --> I[System converts to the base unit, records an IN movement, raises on hand]
  I --> J([Part shows the new quantity; the low-stock badge clears if it was low])

Rules

  • A receipt is one part: there is no multi-line document yet. A delivery of ten parts is ten receipts.
  • The quantity is typed in the unit the goods were counted in. A packaging unit is converted by the backend (10 cases of 24 = 240 cans); the movement stores the 240 and, next to it, the unit and the 10 that were typed, so the movements report and the Excel export show both. A conversion that would need more than three decimals is refused, never rounded.
  • The unit cost is the price per entered unit in VND, optional (0 means “not recorded”). The system stores it per base unit (case price ÷ factor), so a later average cost compares like with like.
  • The movement is append-only: it is never edited. The balance changes in the same database transaction as the movement, so the two cannot disagree.
  • There is no supplier, number or print on a receipt yet; those come with the document in Phase 7.

Exceptions

SituationWhat happens
Quantity zero, negative or not a numberVALIDATION_ERROR
The unit is not a packaging unit of that part, or is the base unit (leave the unit empty instead)VALIDATION_ERROR
A mistake after savingA receipt cannot be edited or deleted. Check the quantity before saving; corrections (counts, write-offs, returns) are made with Release 7.1 documents (P13)
The subscription is read-onlyRefused until it is renewed (P2, P3)

From Release 7.0: the goods receipt document

flowchart TD
  A([Delivery arrives]) --> B[New goods receipt: date, delivery note number, optional default supplier]
  B --> C[Add lines by part search]
  C --> D[Each line: part, unit and quantity, unit cost, supplier]
  D --> E{Every line has a supplier?}
  E -- no --> X[Completing is blocked: pick the supplier on the line]
  X --> D
  E -- yes --> F[Save as draft any time]
  F --> G[Complete]
  G --> H[Movements per line, stock and weighted-average cost updated, part's last cost per supplier updated]
  H --> I{Receipt entered wrongly?}
  I -- yes --> J[Cancel: movements reversed, cost recalculated, audited]
  • The receipt has a number from the garage’s template and the status draft → completed → cancelled. A draft changes nothing in stock. A completed receipt cannot be edited, only cancelled.
  • Every line records its supplier (a supplier of the garage). The header supplier is only a default; a part’s default supplier pre-fills a new line; one delivery from several suppliers is one document.
  • Completing a line adds its base quantity and updates the part’s weighted-average cost: new average = (quantity on hand × old average + received quantity × received cost) ÷ (quantity on hand + received quantity). Each movement keeps its own cost, so past figures never change. Cost is shown only with inventory.cost.read.
  • Cancelling a completed receipt posts reversing movements and restores the previous quantity and average; it is audited. It is refused when part of its stock has already been issued, because the stock would go below zero.
  • Lines can also be added by scanning a barcode or by Excel import (P14, Release 7.2). Purchase orders and supplier payables come later (Phase 13); supplier returns with Release 7.1.

Requirements: FR-INV-01, FR-INV-03, FR-INV-05, FR-CAT-02; Phase 7: FR-INV-06, FR-INV-07, FR-INV-08, FR-INV-19. API: API Specification Section 8 (goods receipts, Section 8.3; stock issues, Section 8.5). Design: Database Design Sections 5.3, 5.4, 5.10 and 5.12. Scenarios: E2E-INV-011 to E2E-INV-015.

14. P12 — Issuing parts to a repair order

Actors: the system (creates and posts the draft), inventory staff, service advisor. Trigger: a repair order with part lines is created, and parts leave the shelf.

Up to Release 6.x: issued when the order is closed

flowchart TD
  A([Repair order with part lines]) --> B[Lines are completed, then Close]
  B --> C[repair_order.closed event]
  C --> D{Part line linked to a catalogue part?}
  D -- no, free text --> E[Nothing is deducted]
  D -- yes --> F[OUT movement per part in the base unit, balance decreases]
  F --> G[stock.issued event]

Rules

  • Parts leave stock when the repair order is closed, not when it is created or started. Only part lines that point to a stocked part deduct anything; a free-text part line and every labor line deduct nothing.
  • The line quantity is already in the base unit (a quote or repair-order line typed in cases is converted when it is saved; the entered unit stays for printing), so 30 typed cans deduct 30, and one case deducts 24.
  • The issue is idempotent: a redelivered close event cannot deduct twice (a unique movement per repair order, part and order id).
  • An issue never fails for lack of stock. On hand can go below zero, which shows as low stock; the garage corrects it with a receipt. Warning the advisor in time is what the Phase 7 reservation is for.

Exceptions

SituationWhat happens
Closing with a line not completedFAILED_PRECONDITION; complete the lines first
The close event arrives twiceThe second is ignored

From Release 7.0: the draft stock issue (phiếu xuất kho)

flowchart TD
  A([Repair order created]) --> B[A numbered draft stock issue is created with one line per part line]
  B --> C[Draft reserves the parts: on hand unchanged, available drops]
  C --> D{When is it posted?}
  D -- order closed, default --> P
  D -- Issue now, by inventory staff --> P
  D -- order started, garage setting --> P
  P[Post: OUT movements with a cost snapshot per line, balance decreases, reservation consumed] --> Q([Corrections are returns or adjustments, P13])
  • The draft is the reservation: a part shows on hand, reserved (in draft issues) and available (the difference).
  • Posting writes the movements and a cost snapshot per line; it is idempotent. After posting the issue is read-only.
  • The garage chooses when it posts (stock_issue_mode: on close by default, or when work starts, which is the first move of a line out of open); inventory staff can always post earlier with Issue now when the parts physically leave the shelf.
  • A stock issue never blocks a repair order: when the reservation is more than the stock on hand, available drops below zero and the order is not held back. From Release 7.3 such an order is shown as waiting on parts (P15).
  • Closed in Release 7.3. Lines changed on an order, and a cancelled order, now reach the draft (P15).

Requirements: FR-INV-02, FR-INV-03, FR-CAT-02; Phase 7: FR-INV-15, FR-INV-20. API: API Specification Sections 7 and 8; Event Design (repair_order.created, repair_order.started, repair_order.closed, stock.issued). Design: Database Design Sections 5.4, 5.11 and 11.6; High-Level Design Section 3.1. Scenarios: E2E-INV-008, E2E-INV-017 to E2E-INV-019.

15. P13 — Counting stock and correcting it (Release 7.1)

Actors: inventory staff (inventory.count, inventory.adjust), owner. Trigger: a periodic count, or a loss, damage, supplier return or unused part found. Until Release 7.1 stock changed only through receipts (P11) and repair-order issues (P12); the adjustments below are in place from Release 7.1.

flowchart TD
  A([Need to correct stock]) --> B{What happened?}
  B -- periodic or spot check --> C[Stock count document]
  B -- lost, damaged, expired, internal use, warranty --> D[Write-off with a reason]
  B -- goods sent back --> E[Return to supplier]
  B -- part not used on an order --> F[Return from the repair order]
  C --> C1[Add lines by search, category or barcode; type the counted quantity]
  C1 --> C2[Each line shows matched, over or short; drafts can be saved and merged]
  C2 --> C3[Complete: adjustments make stock equal the count, value of the difference recorded]
  D --> G[Reduces stock at average cost, numbered document]
  E --> H[Reduces stock, records the supplier credit, linked to the receipt if known]
  F --> I[Restores stock and reverses the cost, reason recorded]

Rules

  • A count compares the counted quantity with the system quantity at the time of counting: the system quantity is recorded when a part is added and again each time its counted quantity is typed, and completing the count takes a final snapshot. Each line shows matched, over, short or uncounted. A count can be saved as a draft and several drafts can be merged into the first one: a part on more than one draft keeps one line with the counted quantities added up, and the other drafts are cancelled with a note.
  • Completing a count is refused while a line is uncounted (STOCK_COUNT_HAS_UNCOUNTED_LINES), unless the user chooses to skip the uncounted lines, which then stay as they are. Completing posts one adjustment per differing line, so on hand equals the count, and values the difference at the part’s current average cost. Adjustments never move the average cost. A completed count cannot be edited.
  • Cancelling a draft only closes it. Cancelling a completed count posts the reversal; it is refused (INSUFFICIENT_STOCK_TO_REVERSE) when the stock the count added has since been used.
  • A write-off is posted when it is created, with its reason, at the average cost of that moment. It is refused (INSUFFICIENT_STOCK) for any line that is more than the quantity on hand. Cancelling it puts the parts back at the cost they left with.
  • A return to supplier is posted when it is created. The credit for a line is the cost typed, else the cost of the same part on the linked completed receipt of that supplier, else the average cost. The credit note number is typed when the return is made and does not change afterwards. Cancelling it puts the parts back.
  • A return of unused parts from a repair order is possible only once its stock issue is posted (STOCK_ISSUE_NOT_POSTED). It restores the stock at the cost the line left with, and the total returned from a line cannot exceed what was issued on it (RETURN_EXCEEDS_ISSUED). The reason is required (up to 500 characters). The return is immutable and does not change the repair order’s lines or what the customer is billed.
  • Counting and adjusting are separate permissions, so a counter does not need to be able to write off stock. The manager and inventory staff hold both by default; a technician or an accountant does not.
  • Every adjustment is an immutable movement with the user, the document and the reason; the part’s stock card shows them in order, together with receipts and issues.
  • Quantities in a count or adjustment may be entered in any of the part’s units and are converted like any other (P11). A count line may be counted at zero, which is a real count of none.
  • A count can also be filled by scanning (the part is added when missing and the cursor goes to its counted quantity) or by Excel import (P14).

Requirements: FR-INV-09, FR-INV-10, FR-INV-11, FR-INV-12. Plan: Release Plan Phase 7, Release 7.1. Design: Database Design Section 5.13. Scenarios: E2E-INV-020 to 029 in 07_Inventory.md.

16. P14 — Scanning barcodes and importing from Excel (Release 7.2)

Actors: inventory staff, owner (and the manager). Trigger: a garage starts using VietGara and has its parts in a spreadsheet; a delivery, or a shelf, is to be entered quickly with a scanner.

flowchart TD
  A([Need to enter many parts or lines]) --> B{How?}
  B -- one by one, with a scanner --> S[Scan: a USB scanner types into the field, the phone camera reads the label]
  S --> S1{Code known?}
  S1 -- no --> S2[Told on the spot; type another or add the barcode to the part]
  S1 -- inactive part --> S3[Told: the part is inactive]
  S1 -- yes --> S4[Receipt: a line, or one more; count: the part and its counted quantity]
  B -- from a file --> D[Download the template for parts, a goods receipt or a stock count]
  D --> E[Fill it in and choose the file]
  E --> F[Preview: every problem listed with row, column and reason, nothing saved]
  F --> G{Any problem?}
  G -- yes --> H[Fix the file and choose it again]
  H --> F
  G -- no --> I[Confirm: all rows are imported in one step]
  I --> J[Parts: created, opening stock posted as one completed receipt]
  I --> K[Receipt or count file: one draft document to review and complete]

Rules

  • A part can have up to 20 barcodes. A code is unique in the garage (compared without regard to case) and is for the base unit or for one of the part’s packaging units, so scanning a case code on a receipt adds cases. Deleting a part frees its codes.
  • Scanning needs no special permission beyond reading parts. An unknown code is told on the spot; a code of an inactive part says the part is inactive. A scan on a receipt adds a line, or one more of the same part and unit; on a count it adds the part when missing and moves the cursor to its counted quantity. The phone app scans into counts, write-offs, returns and the stock list; it does not edit barcodes.
  • An import has three steps and the preview writes nothing. The preview reports every problem of the file at once, so the user fixes the file once and chooses it again. A file with any problem is refused as a whole on confirm (IMPORT_FILE_ALREADY_IMPORTED and IMPORT_FILE_INVALID are the two refusals); nothing is partly imported.
  • The parts file creates parts only. A SKU or barcode already in the garage, or repeated in the file, is an error. Missing units and part categories are created. A row with an opening quantity names a supplier; all such rows become one completed goods receipt (each line with its own supplier), so stock and the average cost follow the usual rules (P11). The unit cost needs inventory.cost.read.
  • The goods receipt and stock count files each make one draft; the user reviews it and completes it like any other document. Lines are found by SKU or barcode; the columns that belong to the whole document must agree wherever they are filled.
  • Permissions follow the file: catalogue write for parts, plus inventory write for opening stock and cost access for costs; inventory write for receipts; inventory count for counts. A role that may not use a column is told so before anything is saved.
  • A confirmed file cannot be confirmed again unchanged. Every confirmed import is audited and kept in the import history.
  • Limits: 5 MB and 5,000 rows per file.

Requirements: FR-INV-13, FR-INV-14, FR-INV-06, FR-INV-12. Plan: Release Plan Phase 7, Release 7.2. API: API Specification Sections 8.1 and 8.10. Design: Database Design Section 5.14. Scenarios: E2E-INV-030 to E2E-INV-038 in 07_Inventory.md.

17. P15 — Quoting with jobs, and changing, waiting and cancelling an order (Release 7.3)

Actors: service advisor (quote.write, repair_order.write), owner or manager (the job catalogue), inventory staff (receipts). Trigger: a customer asks for a common piece of work; or an order needs another part, or the customer drops it; or the stock for an order is not on the shelf.

flowchart TD
  A([Customer asks for a job]) --> B[Pick the job on the quote]
  B --> C[Labor line + a line per suggested part; hidden consumables marked]
  C --> D[Edit lines as needed; send for approval]
  D --> E[Customer decides on the billable lines]
  E --> F[Convert: approved lines + the consumables of approved jobs become the order]
  F --> G[Draft stock issue reserves the parts]
  G --> H{Enough stock?}
  H -- no --> I[Order waits on parts]
  I --> J[Stock arrives, or another order is cancelled]
  J --> K[Order is back to open]
  H -- yes --> K
  K --> L{Change of plan?}
  L -- another part or quantity --> M[Add, edit or remove a line; the draft follows]
  L -- customer drops it, no work started --> N[Cancel: reservation released]
  L -- carry on --> O[Work, complete, close: the issue posts]

Rules

  • A job is a type of work with a standard labor price, a duration and up to 30 suggested parts. Owners and advisors maintain the catalogue; a job that a quote or an order used is only deactivated when deleted, and an inactive job leaves the picker but stays on what used it. A part or packaging unit that a job suggests cannot be deleted or removed from under it.
  • Picking a job on a draft quote adds a labor line and a line per active suggested part, at today’s prices and in the suggested unit. The advisor edits them like any line (change 4 litres to 5, change a price, remove a line).
  • A suggested part can be a hidden consumable (grease, screws, cleaning fluid): it is stocked and deducted but not billed. The customer does not see it on the approval page or the PDFs and does not decide on it; it follows its job, so it reaches the order only when a line of the job was approved.
  • Lines of an open order can be added, edited and removed until it is closed or cancelled; the draft stock issue follows part lines. After the issue is posted (the garage’s start-of-work mode, or “Issue now”) the parts are fixed; labor and prices can still change.
  • Waiting on parts. If the stock on hand cannot cover the draft issue, the order is shown as waiting on parts and the issue shows how much is short. Orders are served first come, first served per part. Stock arriving, or another order’s reservation freed, brings the order back by itself. It never blocks the work.
  • Cancelling an order is possible while it is open or waiting on parts and no line has started. Its reservation is released at once; the quote stays converted (the customer gets a new quote); the order stays on the vehicle’s history as cancelled and is read-only. Once work started, the order is completed or closed, or its stock issue is posted, it cannot be cancelled: parts that already left the shelf come back through a return (P13).

Exceptions

SituationWhat happens
Job inactive, or the quote is not a draftRefused (JOB_INACTIVE, or the quote cannot be changed)
A suggested part was deactivated sinceThat suggestion is skipped
A quote would pass 100 linesRefused; remove lines first
Part line changed after the issue is postedRefused (STOCK_ISSUE_POSTED)
Removing the last billable line of an orderRefused; cancel the order instead

Requirements: FR-CAT-05, FR-CAT-06, FR-CAT-08, FR-INV-15, FR-REP-07, FR-REP-08. Plan: Release Plan Phase 7, Release 7.3. API: API Specification Sections 7, 8 and 16.8. Design: Database Design Sections 4 and 5.15; Event Design (repair_order.lines_changed, repair_order.cancelled, stock_issue.shortage_changed). Scenarios: E2E-CAT-010 to E2E-CAT-013, E2E-QUO-015 to E2E-QUO-017, E2E-REP-013 to E2E-REP-018.

18. Keeping this document current

A procedure changes in the same pull request as the requirement or the design it belongs to. Diagrams are Mermaid, like the rest of the documentation. A new multi-party procedure (for example a supplier link in Phase 17, or the claim flow) gets its own section here, with the same parts: actors, trigger, diagram, rules, exceptions and references.