RELEASE PLAN — PHASE 5
VietGara — Customer & Garage Foundations
Version 5.0 — October 9, 2026 · Cloud: Google Cloud Platform (GCP) · Backend: Go
1. Introduction
Phase 5 answers four questions before the product grows further:
- Who is the customer? A vehicle owner can be a garage’s customer without opening a VietGara user. When they do have a user, the garage links to it with their consent. Suppliers and insurers are different things and get their own records.
- Who is the garage legally? Documents and, later, e-invoices need the garage’s legal name, tax ID and logo, and the customer’s personal or company tax IDs.
- How are objects removed and files kept? One rule decides between deleting and deactivating, and one registry knows every uploaded file and who owns it.
- How does the platform get paid before online payment exists? An owner pays for a plan by bank transfer and proves it with a screenshot; a platform admin reviews the request and the plan changes at once on approval.
The web consoles and the owner and admin apps change; the technician and customer apps keep working against the same API.
2. Overview of the 3 Releases
| Release | Name | Main Modules | Demoable Value |
|---|---|---|---|
| 5.0 | Platform Rules & Garage Identity | Shared lifecycle rule, Billing (vouchers), Tenant (legal profile), file registry, insurance removal | Delete or deactivate by one rule; a printed payment voucher with the garage’s legal header; no insurance code left |
| 5.1 | Flexible Customers & Tax Profiles | Customer & Vehicle, Customer BFF | Find a customer by phone or e-mail while quoting, link to a user or keep a walk-in customer; bill a company with its tax ID; lock a used vehicle’s identity fields |
| 5.2 | Subscription Plan Changes by Manual Payment | License, File registry, Notification | An owner picks another plan or renews, transfers the amount and submits the evidence; the platform admin approves and the new plan applies at once, with unused days credited |
3. Release Details
Release 5.0 — Platform Rules & Garage Identity
- Delete or deactivate (FR-CAT-07). One shared helper decides what “delete” does:
- an object referenced nowhere is soft-deleted (
deleted_at); - an object in use gets
is_active = false, disappears from pickers and stays on the documents that use it; - staff reactivate it later;
- the API answers
DELETEDorDEACTIVATED, and the confirmation dialog says which one will happen. - Applied now to customers, vehicles and suppliers. Each later entity (units, categories, parts, jobs, tax profiles, files) adopts it when introduced. The unique SKU or code of a soft-deleted object can be reused.
- Soft deletion is not erasure of personal data. FR-CUS-05 (Phase 13) adds real anonymization.
- an object referenced nowhere is soft-deleted (
- Insurance removal. The product is in development and no insurer data exists, so the original migrations are edited in place (the rule of Phase 3):
- remove the
settlements.insurer_*columns and theinsurervalues of thepayer_typeandpartner_typeenums from migrations 000007 and 000018; - a settlement has one payer, the customer, so
settlement_payments.payer_typegoes away; - remove the allocation use case, the insurer fields of the settlement PDF, the insurer type of partners and their tests, mocks and generated code;
- proto: remove the fields and mark them
reserved; accept thebuf breakingfinding in this change; - web consoles, owner app and E2E: remove the insurer split and the insurer type of the Partners screen;
- docs: FR-BIL-02 is withdrawn and FR-INS is rewritten (done in FRD 1.7).
- remove the
- Suppliers are not customers (FR-SUP-01). The
partnerstable, which holds suppliers and insurers in one place, becomessuppliers(the type enum disappears with the insurer type):partner.read/partner.writebecomesupplier.read/supplier.write,/partnersbecomes/suppliers, and the supplier code moves out of the Customer module into a small Supplier module. A supplier keeps the fields it has (name, tax code, contact) plus bank details. Insurers are not suppliers either: they return in their own table in the Insurance module (Phase 17). The web consoles and the owner app rename the Partners screen to Suppliers. - Row audit columns (NFR-MT-07). Every business table gets
created_at,created_by,updated_at,updated_by, filled by a trigger from the session user (app.current_user_id), so no code path can forget. Append-only tables (stock ledger, audit log, outbox, notification log, claim and support events) keep the created pair only; link tables (role_permissions,staff_group_members,item_units) get the created pair, and the updated pair if rows can change; a system job writes NULL. Todayusers,plans,subscriptions,entitlements,staff_invitations,quote_lines,repair_order_lines,stock_balance,document_sequencesand a few more lack some of them. A schema test (pgtest) fails when a new table has neither the columns nor an allowlist entry. - User display name (FR-IAM-12). The profile already lets a user edit their full name. This release adds
users.full_name_source(googleoruser) so a later Google sign-in never overwrites an edited name. Invoice, voucher and payment names are not the user’s name; they come from the business records (garage legal name, customer tax profile, voucher payer). - Fix the quantity and cost types. Quantities are stored as
VARCHAR(12)decimal strings instock_transactions,quote_linesandrepair_order_lines, andstock_balance.quantity_on_handisNUMERIC(14,2);items.reorder_thresholdis an integer. All becomeNUMERIC(14,3)in the same pass, because units and conversion (Release 6.0) need real decimals and valuation and profit reports aggregate quantities in SQL.stock_transactions.unit_costbecomesNUMERIC(18,4)(a cost rounded to whole dong would drift once average cost arrives in Phase 7). - Garage legal profile (FR-TEN-06). Add legal name, legal address, representative and bank account to the garage (the tax ID is already stored); use them, with the logo, in the header of every PDF.
- Payment vouchers (FR-BIL-05/06).
settlement_paymentsgains a garage-sequential voucher number (fromdocument_sequencesand a numbering template in the garage settings), payer name and address, reason for payment, collector, and a void flag with reason.- A voucher PDF in the 01-TT layout shows the amount in figures and words and signature blocks. Voiding recalculates the settlement and is audited.
- File registry. Replace “URL in a business column” with a
filestable (id,garage_id,user_id,kind,entity_type,entity_id,key,content_type,size,sha256,created_by,deleted_at) under the same row-level security as other garage data.- Key layout:
garages/{garageId}/{kind}/{entityId}/{uuid}.{ext}andusers/{userId}/{kind}/{uuid}.{ext}. - Business rows store the file id or key, never the URL, so a CDN or bucket change breaks nothing.
- Public kinds (logo, part image) stay public. Every later kind (repair photo, customer document, claim document, export) is private and served through short-lived signed URLs; the local backend checks authorization itself.
- Deleting a garage purges its prefix. The existing logo moves into the registry in this release.
- Key layout:
- Frontend (web console and owner app): legal profile in garage settings, voucher numbering template, voucher list and PDF on the settlement, void action, and the delete-or-deactivate dialog.
- Definition of Done:
- the Partners screen is now Suppliers, with no insurer type, and
supplier.*permissions apply; - deleting an unused vehicle soft-deletes it; deleting a customer with a quote deactivates it, and staff can reactivate it;
- recording a payment issues a numbered voucher; voiding it restores the outstanding amount;
- the voucher and the quote PDFs show the garage’s legal header and logo;
- searching the backend, proto, web and apps for “insur” finds nothing outside the migration history;
- all existing tests pass after the removal.
- the Partners screen is now Suppliers, with no insurer type, and
Release 5.1 — Flexible Customers & Tax Profiles
- Customer without a user (FR-CUS-01, FR-CUS-06). The customer table is already garage-owned with optional e-mail and phone. This release makes that an explicit product rule and adds the link to a user:
- a
customer_user_linkstable (customer_id,user_id, statusrequested/linked/declined/unlinked, channelotp/app/auto_email); suppliers do not use it (they get their own link in Phase 17); - a search that matches by phone or e-mail inside the garage and, platform-wide, tells only that a user exists (a lookup function like
find_customers_by_email, never returning user data); - a link request that the user confirms by OTP or in the app; a verified user e-mail still links matching profiles automatically at sign-in, as the Customer BFF does today;
- unlink by either side;
- phone numbers are normalized to E.164 and phone matching uses verified phones only (it depends on Release 2.4’s phone login; until then, e-mail only).
- a
- Customer tax profiles (FR-CUS-07). One
customer_tax_profilestable with a kind: at most one personal profile and any number of company profiles (legal name, tax ID, address, e-mail for invoices, default flag). It replaces a separate personal tax ID column. The tax ID is format-checked (10 digits, or 13 with the branch suffix). Quotes and settlements pick the billing party and keep a snapshot; PDFs and vouchers print it. A settlement can be marked invoice requested only when the chosen profile is complete, otherwise it stays a retail receipt (INVOICE_DATA_INCOMPLETE). A tax profile follows the lifecycle rule. - Vehicle edit lock (FR-CUS-02, FR-CUS-09). A vehicle’s identity fields (plate, chassis, engine number, make, model, year) can be edited only while no live document refers to it: quote, repair order, settlement, booking or claim; a soft-deleted quote does not count. The usage is computed by a query, not stored, and the API returns
editableflags and the locked field names. Colour, notes and odometer stay editable. A new permissioncustomer.correctlets chosen garage staff correct a locked field with a mandatory reason (audited; documents keep their snapshot); the manager role has it by default. A vehicle that really changed is deactivated and replaced; a sold vehicle moves to another customer with an owner-change action that keeps its history. - Frontend (web console and owner app): the quote customer picker becomes one search box with three outcomes (use the profile, request a link with the found user, or create a walk-in customer); tax profiles on the customer screen; billing party on the quote and settlement.
- Customer app (small change): a list of garages that hold the user’s customer profile, and accept or decline a link request (FR-CAPP-05).
- Docs: Vision actors, FRD, Database Design, API Specification, Release 2.4’s wording and E2E scenario 04.
- Definition of Done: a vehicle used by a live quote refuses a plate edit, while a vehicle whose only quote was deleted accepts it; a staff member with
customer.correctcorrects a locked plate with a reason, and the audit log shows it; a settlement cannot be marked invoice requested for a company profile without a tax ID. An advisor starts a quote for a walk-in customer without any user. Another advisor searches by e-mail, finds a user and sends a link request; the customer accepts in the app and sees the garage’s vehicles. A company customer with two tax profiles is billed under the chosen one, and its tax ID prints on the PDF.
Release 5.2 — Subscription Plan Changes by Manual Payment
- Rule (FR-LIC-02, FR-LIC-07, FR-LIC-08). An owner changes plan or renews by paying the platform’s bank account and submitting a payment request; the plan takes effect at once when a platform admin approves it. There is no scheduled change and no waiting for the period end. Upgrades, downgrades and billing-cycle changes follow the same rule. Online payment (Phase 16) only replaces the manual transfer and the review with a gateway charge.
- Request. The owner starts a request and sees the amount due, the platform’s bank details and QR code, and a unique transfer reference (for example
VG-7K2M9Q) to write in the transfer note; the request waits for the payment. After the transfer the owner submits the payer, bank, account, amount, the bank’s transaction reference, the date and a screenshot. One request is open per owner (one still waiting for payment is replaced by a new one); the owner can cancel it before review; a submitted request never expires because the money has moved. When the credit covers the whole price nothing is transferred, and the admin still approves. - Validation. The new plan’s limits (garages, staff) are validated when the request is submitted and again at approval; an invalid upgrade or downgrade is refused with
PLAN_LIMIT_REACHEDnaming the limits. The trial plan is given once, at sign-up, and can never be chosen again. - Amount (FR-LIC-09). New plan price minus the credit for the unused days of the current period: whole unused days ÷ days of the period × the amount paid for it, rounded down to whole dong; a trial period was paid 0. The amount is quoted at submission and honoured at approval. A surplus (a downgrade) becomes account credit, applied automatically to the next payment; refunds are manual and outside the system.
- Approval. In one transaction under the subscription lock: the plan switches, a new period of the plan’s billing cycle starts at approval, the paid amount is recorded on the period, the credit ledger is updated and the entitlement cache is refreshed. The unused days of a trial are lost. A renewal of the current plan before its end extends from the old end date, so there is no gap; one after expiry restarts from approval and reactivates the subscription. The platform admin’s manual extension keeps working for corrections.
- Review (FR-LIC-08). A review queue in the admin web and the admin app. The admin approves, or rejects with a reason the owner sees (the owner may submit again). When the transferred amount differs from the quote the admin sees a warning, resolves it with the owner, and may approve with a note. The admin is e-mailed on submission and reminded every day while requests wait.
- Notices. The owner is notified 7 days before the period ends with the payment instructions, and on approval and rejection.
- Platform payment details (FR-LIC-10). Bank, account number, holder, branch, transfer instructions and an uploaded QR image, edited in the admin web and the admin app; every change is audited and a request keeps the instructions shown when it started.
- Data.
plan_payment_requests,platform_payment_settings, a paid-amount snapshot and a credit balance on the subscription, and an append-only credit ledger. Evidence is a private file kind and the QR a platform file in the file registry of Release 5.0. - Frontend (web console and owner app): the plan screen shows the current plan, usage, credit and the open request; choosing a plan shows the quote, the bank details with the QR code and the evidence form; the request’s status and rejection reason are shown. Admin web and admin app: the review queue with the evidence and the amount comparison, approve and reject, and the platform payment details with the QR upload.
- Definition of Done: an owner on Basic with 10 unused days requests Pro, sees the amount due after the credit and the transfer reference, transfers and submits the screenshot; a request that exceeds Pro’s limits is refused at submission. The admin sees the request in the queue with a mismatch warning when the amount differs, approves it, and the owner’s plan and limits change at once with a new period starting at approval. A downgrade leaves the surplus as credit that is used by the next payment. A rejected request shows its reason and can be submitted again. An owner who already used the trial cannot choose it again.
4. Dependency Diagram
flowchart TD P4(["Phase 4 complete"]) --> R50["Release 5.0<br/>Rules, legal profile, vouchers, files"] R50 --> R51["Release 5.1<br/>Flexible customers, tax profiles"] R50 --> R52["Release 5.2<br/>Plan changes by manual payment"] R51 --> DONE(["Phase 5 complete"]) R52 --> DONE
Release 5.0 comes first: the lifecycle rule, the file registry and the legal profile are used by everything after it, and removing insurance early stops more code from building on the payer split. Release 5.1 uses the lifecycle rule and the voucher payer data of 5.0. The supplier rename of 5.0 is independent of 5.1. Release 5.2 needs the file registry of 5.0 (the evidence and the QR code) and is independent of 5.1.