INTEGRATION DESIGN
VietGara — Multi-Tenant SaaS Platform for Auto Garage Management & Maintenance
Version 1.3 — October 9, 2026 · Cloud: Google Cloud Platform (GCP) · Backend: Go
1. Introduction
This document describes the integration design with third-party systems: transactional e-mail, Google sign-in, file storage, e-invoice providers, payment gateways, and insurance companies. General principle: each partner has its own adapter implementing a common business interface, isolating the risk of external API changes from the core business logic.
2. Integrations in Phase 1
2.1. Transactional E-mail (ADR-011)
| Environment | Provider | Configuration |
|---|---|---|
| Local development, integration tests | Mailpit (SMTP catcher with a web inbox on port 8025) | MAIL_PROVIDER=smtp, SMTP_HOST, SMTP_PORT |
| Staging, production | Mailjet Send API v3.1 | MAIL_PROVIDER=mailjet, MAILJET_API_KEY, MAILJET_SECRET_KEY (Secret Manager), MAIL_FROM_ADDRESS on a domain validated with SPF and DKIM |
type Sender interface { Send(ctx context.Context, msg Message) error // smtp or Mailjet, chosen by MAIL_PROVIDER}- Templates (
email_verification,password_reset,staff_invitation) are embedded per language (vi,en) and rendered in the recipient’slocale. - OTP e-mails are sent synchronously: if sending fails, the OTP is discarded and the request fails, so the user can retry at once. Invitation e-mails are sent after the invitation commits; a failure is logged and the invitation remains visible in the invitee’s console.
- From Release 1.5 the Notification Service takes over non-OTP e-mails from events (e.g. the registration confirmation and
subscription.expiring_soon). Staff invitation e-mails (staff.invited) stay a direct send after commit in Release 1.x (Event Design, Section 4).
2.2. Google Sign-in
sequenceDiagram
participant W as Owner web
participant P as Popup window
participant B as Backend (Identity)
participant G as Google OAuth 2.0
W->>P: open /api/v1/auth/google/login
P->>B: GET /auth/google/login
B-->>P: redirect to Google (state cookie)
P->>G: consent
G-->>P: redirect /auth/google/callback?code&state
P->>B: callback
B->>G: exchange code, read verified e-mail
B->>B: find or create the user (verified), issue a session
B-->>P: page posting {type, session} to window.opener (same origin)
P-->>W: postMessage → signed in
Configuration: GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET and APP_BASE_URL (the owner web’s host, e.g. https://vietgara.viettechno.com). The redirect URI is APP_BASE_URL + /api/v1/auth/google/callback, registered in the Google Cloud console; the popup posts its result to APP_BASE_URL’s origin. Mobile apps sign in with an ID token instead (GOOGLE_MOBILE_CLIENT_IDS, POST /api/v1/auth/google/id-token).
2.3. File Storage
| Environment | Backend | Notes |
|---|---|---|
| Local, staging | Local disk (STORAGE_BACKEND=local) | Persistent volume; served by the backend at /api/v1/files/… |
| Production | Cloud Storage (STORAGE_BACKEND=gcs) | Public bucket STORAGE_GCS_BUCKET (logos and other public kinds), public URL prefix STORAGE_PUBLIC_BASE_URL (bucket or CDN); private bucket STORAGE_GCS_PRIVATE_BUCKET served as V4 signed URLs; Workload Identity credentials |
On local disk and staging, private objects are served by the backend with an HMAC signature keyed by STORAGE_SIGNING_KEY (a secret). File kinds and their access are listed in Database Design, Section 6.7.
Uploads are validated before storage (garage logos: PNG, JPEG or WebP, at most 1 MB) and stored under random object names.
3. Adapter Pattern Design
// Common business interface, defined in the core packagetype EInvoiceProvider interface { IssueInvoice(ctx context.Context, req IssueInvoiceRequest) (InvoiceResult, error) GetInvoiceStatus(ctx context.Context, invoiceRef string) (InvoiceStatus, error) CancelInvoice(ctx context.Context, invoiceRef string, reason string) error}
// Each provider implements this interface separatelytype VNPTAdapter struct { client *vnpt.Client }type ViettelAdapter struct { client *viettel.Client }
func (a *VNPTAdapter) IssueInvoice(...) (...) { /* calls VNPT's own API */ }func (a *ViettelAdapter) IssueInvoice(...) (...) { /* calls Viettel's own API */ }
// Factory selects the adapter based on each garage's configurationprovider := factory.GetProvider(garage.EInvoiceProviderCode)result, err := provider.IssueInvoice(ctx, req)Adding a new provider (MISA, BKAV, etc.) only requires writing one new struct implementing the interface, without touching the business logic in Billing & Settlement or the E-Invoice Service — following the Open/Closed Principle.
4. E-Invoicing Integration
4.1. Invoice Issuance Flow
sequenceDiagram
participant E as E-Invoice Service
participant P as VNPT / Viettel API
E->>E: build invoice payload
E->>P: signed issue request
P-->>E: invoice_number, signature status
E->>E: save invoice record
E-)E: publish invoice.issued
alt error or timeout
E->>P: retry (exponential backoff, up to 3 attempts)
E-)E: status = failed, publish invoice.issue_failed
end
4.2. Circuit Breaker
When the error rate from a provider exceeds a threshold (e.g. >50% of requests fail within 1 minute), the circuit breaker trips to the ‘open’ state — temporarily suspending direct calls, failing fast, and queuing requests, to avoid slowing down the entire settlement flow because one partner is having an outage. After a configured cooldown, it moves to ‘half-open’ to retry.
4.3. Providers by Rollout Phase
| Phase | Providers |
|---|---|
| Phase 16 | VNPT, Viettel |
| Phase 17 | MISA, BKAV (based on real onboarding demand) |
5. Payment Gateway Integration
5.1. Online Payment Flow
sequenceDiagram actor C as Customer App participant A as Payment Adapter participant G as VNPay / Momo / ZaloPay C->>A: request payment link A->>G: create transaction G-->>A: payment_url A-->>C: payment_url C->>G: redirect, complete payment G->>A: webhook: payment success (signed) A->>A: verify signature, dedupe by transaction_id A-)A: publish payment.confirmed A-->>C: push "Payment successful"
- Webhooks are signature-verified (HMAC) as provided by the payment gateway, preventing forged webhook requests.
- Idempotency: each webhook has a unique transaction_id, processed exactly once even if received multiple times due to gateway retries.
- Multiple gateways: a garage can configure several gateways at once; the system prioritizes the primary gateway and automatically switches to a backup gateway when the primary fails repeatedly (per RISK-3RD-03 in the Risk Assessment).
6. Insurance Integration (Phase 17)
In Vietnam the vehicle owner files the claim with the insurer; the garage supports it with the quote, the repair and the documents. Most insurers have no open API, so the design is a claim-tracking workflow first, and an integration second.
6.1. Claim Procedure Modelled
| Step | What happens | What VietGara records |
|---|---|---|
| 1. Report | The owner secures the scene, calls the insurer’s hotline, and notifies it in writing, by app or by e-mail within 5 working days of the incident (Decree 67/2023/ND-CP). Serious or disputed accidents also need the traffic police’s report | Incident date, place, description, notification date and a countdown to the 5-working-day limit |
| 2. Assessment | The insurer’s assessor inspects the vehicle within 24 hours, at the scene or at the garage, and writes the assessment report | Assessor, date and the attached assessment report |
| 3. Quote and agreement | The garage quotes the repair; the insurer and the owner or garage agree the cost and repair method | The insurer-agreed quote, a versioned snapshot of the garage quote |
| 4. Claim file | Vehicle registration, driver’s licence, ID card, insurance certificate or e-policy, accident report, damage and assessment reports, repair invoice and vouchers | A document checklist with private attachments; the owner can upload from the Customer App |
| 5. Approval and payout | Approval in 3-5 days (small loss) or 7-15 days (complex). The owner pays only the deductible or out-of-cover part, or the insurer pays the owner | Claim status, the settlement split and the insurer’s payments, which feed Receivables per claim |
6.2. Claim Tracking (baseline, every insurer)
- A claim is linked to a repair order, a vehicle and an insurer from the garage’s insurers directory (not a customer, not a supplier). Its status moves through reported, assessed, quote agreed, file submitted, under review, approved or rejected, and paid, with an event log.
- Deadlines and the checklist drive reminders by e-mail and push.
- The claim file is packaged as a PDF set according to the insurer’s template (Document/Print) for manual e-mail or portal submission. Staff update the status by hand when the insurer answers.
6.3. API Submission (where the insurer supports it)
sequenceDiagram participant S as Staff participant I as Insurance participant X as Insurer API S->>I: submit claim file I->>X: submit claim (adapter, circuit breaker) X-->>I: claim_id, status X-->>I: status updates (poll or webhook) I-)I: publish claim.status_changed
6.4. Rollout Priority
- Release 17.0: claim tracking for every insurer.
- Release 17.1: PVI, PTI, Bao Viet first (the insurers with the largest affiliated garage networks), then others by real onboarding demand.
7. Integration Security
- Each partner’s credentials (API keys, signing certificates) are stored in Google Secret Manager, never hard-coded or stored in the application database.
- Least-privilege principle: each adapter only has access to its own credentials, not a shared service user across all integrations.
- All third-party requests/responses are logged (with sensitive data masked) to support debugging and audit in case of disputes.
7.1. Print Agent (Release 14.1)
- The agent opens only an outbound secure WebSocket; it never listens on a port.
- It is paired with one garage by a revocable key and accepts jobs for that garage only.
- Jobs are signed by the backend, carry no more data than the print output, and are kept only in a short local queue until printed.
- Printing from mobile apps to LAN printers (ESC/POS over TCP 9100, IPP) stays on the garage network and never passes through VietGara.