Copied verbatim from the repo; the text inside is Vietnamese, as in the source.
D1 — Send reservation → folio in Odoo
[Chủ báo giá] [UI React] [BFF Hono] [Odoo]
| | | |
| (đã Save trip, | | |
| revision seq n, | | |
| snapshot có sẵn) | | |
| | | |
| bấm SEND | reservationBlockedReason (thuần, không mạng), thứ tự: |
| RESERVATION | 'sent' hàng sống (pending/confirmed/unknown) -> khoá, |
| | title "Reservation already sent" |
| | 'update' giá chưa khớp trip -> khoá "Update the price first" |
| | 'save' seq = 0 HOẶC !savedIsCurrent (khác plan) -> khoá |
| | "Save the trip first" |
| | 'login' ẩn danh -> nút VẪN bấm được, mở AuthGate tại chỗ |
| | (Q-004), KHÔNG gọi mạng |
| | navigate /trip/:id/reserve | |
| | GET /api/me -------------> name, login -> điền sẵn contact |
| | GET /api/estimates/:id + | |
| | GET /:id/revisions/:seq | (DB mình, 0 Odoo) so JSON workingTrip |
| | ! khác -> "You have unsaved changes…", Confirm khoá (khác plan) |
| | | |
| sửa contact, bấm | cờ inFlight + disabled: | |
| CONFIRM | bấm đúp = một POST | |
| | POST /api/estimates/:id/ | |
| | submit {seq, contact} =>| 1. owner/phiên ubg_auth? không -> |
| | | 401 {reason:'login'}, KHÔNG cookie |
| | | mới, TRƯỚC mọi thứ (khác plan: |
| | | không ensureOwner) |
| | | 2. zod body ! 422 {fields} (chỉ tên |
| | | trường, không giá trị) |
| | | 3. getScenario(id, owner) ! 404 |
| | | 4. latestSnapshot ! 409 'no-snapshot' |
| | | seq !== latest.seq ! 409 'stale' |
| | | 5. cửa: ODOO_SUBMIT_ENABLED=1 HOẶC |
| | | (FIXTURE_MODE=1 VÀ mode fixture) |
| | | ! đóng -> 503 'closed', KHÔNG ghi gì |
| | | 6. createSubmission -> INSERT pending |
| | | TRƯỚC lời gọi Odoo -> (submission) |
| | | ! 'already' (unique index) -> 409 |
| | | {reason:'already', state} |
| | | 7. gateway.submit({contact, |
| | | trip: revision.trip}) |
| | | =======================================>| POST /v1/booking/submit
| | | timeout 20s (ODOO_TIMEOUT_SUBMIT_MS), | header api-key
| | | retries 0, breaker chung với compute | sanitize trip,
| | |<=======================================| tạo folio + draft
| | | {success, folio_id, order_ids} | quotations
| | | 8. markSubmission (chỉ từ 'pending') |
| | | -> 'confirmed' + folio_id/order_ids; |
| | | setScenarioStatus('submitted') |
| |<== 200 {state:'confirmed', | |
| | folioId, orderIds, | |
| | sample, seq} =========| |
| | navigate /trip/:id/booking (cả khi 409 'already' và 502 'unknown') |
| | GET /:id/submission ===>| latestSubmission(id, owner) — không trả |
| | | contact |
| | "Reservation sent · Folio #123" (fixture: "Folio number pending" |
| | + "Sample — no folio was created"), "What happens next…", |
| | Print / PDF, Back to trip |
| | | |
| (mở lại /trip/:id) | đã đăng nhập -> GET /:id/ | |
| | submission (một lần) ==>| đọc hàng mới nhất theo chủ HIỆN TẠI |
| |<== state =================| |
| | banner trên thẻ giá "Reservation sent" + View reservation; |
| | Send khoá theo state |
| | | |
! Odoo trả HTTP lỗi (4xx/5xx, OdooHttpError) hoặc 200 {success:false} -> 'failed' (+error) |
! -> 502 {reason:'rejected', status}; UI "did not accept… Try again" (hàng mới khi bấm lại)|
! breaker mở (BreakerOpenError, KHÔNG có lời gọi nào đi) -> 'failed' (error 'breaker-open') |
! -> 503 {reason:'busy'}, cho bấm lại (khác plan, D-037) |
! timeout / lỗi mạng -> 'unknown' (Odoo CÓ THỂ đã tạo folio) -> 502 {reason:'unknown'} -> |
! Booking "We could not confirm your reservation… do not send it again"; Send khoá vĩnh |
! viễn ở P2, staff xử (B-034, D-039) |
! markSubmission trả null (hàng không còn pending) -> 500 {reason:'unknown'} |
! fixture mode -> fixture twin trả {success:true, folio_id:null, order_ids:null}, sample:true |
! — hình dạng theo spec, không phải hành vi Odoo (B-033, D-038) |D2 — Submission state machine
(none)
|
| ! cửa đóng (503 'closed') -> KHÔNG có hàng, vẫn (none)
|
| POST /submit — INSERT trước khi gọi Odoo
v
+-----------+
| pending | khoá nút Send
+-----------+
/ | \
200 success / | \ timeout / network
+-----------+ | +-----------------+
v | v
+-----------+ | HTTP 4xx/5xx, +-----------+
| confirmed | | success:false, | unknown |
+-----------+ | breaker mở +-----------+
khoá Send v khoá Send
(vĩnh viễn ở +-----------+ (vĩnh viễn ở P2 —
P2; amend | failed | Odoo CÓ THỂ đã tạo
B-035) +-----------+ folio; staff xử
scenario.status KHÔNG khoá Send — B-034)
='submitted' bấm lại -> hàng pending
MỚI, hàng cũ giữ 'failed'D2 (cont.) — Where the data goes on submit
Reserve form {name, email, phone?} ---> POST /submit body.contact (trim; phone rỗng -> null)
| |
| +--> (submission).contact jsonb — PII, không vào log,
| | không vào submission.error, không trả ở GET
| +==> Odoo SubmitRequest.contact
(revision).trip của seq mới nhất --------------> Odoo SubmitRequest.trip (toOdooTrip; không bao giờ
working trip hay trip trong payload)
Odoo {folio_id, order_ids} ------------------> (submission).folio_id / order_ids
-> 200 {folioId, orderIds} -> trang Booking
gateway mode fixture ------------------------> (submission).sample = true -> "Sample — no folio was created"D3 — ERD: the submission table
+---------------------------------------------+
| submission |
|---------------------------------------------|
| id uuid pk default gen_random_uuid() |
| scenario_id uuid not null |
| fk -> scenario.id on delete cascade |----+
| revision_seq int not null | |
| snapshot_id uuid not null fk -> snapshot.id |----+---+
| state text not null check in ( | | |
| pending, confirmed, failed, unknown) | | |
| contact jsonb not null (name,email,phone) | | |
| -- PII, không log dạng thô | | |
| folio_id int null | | |
| order_ids int[] null | | |
| error text null ('HTTP 4xx', 'odoo: | | |
| success=false', 'breaker-open', 'timeout',| | |
| 'network' — không PII) | | |
| sample boolean not null default false | | |
| submitted_by_role text not null | | |
| submitted_by_ref text not null | | |
| created_at timestamptz not null default now | | |
| updated_at timestamptz not null default now | | |
+---------------------------------------------+ | |
v v
+--------------------------+ +--------------------------+
| scenario (đã có, P1) | | revision / snapshot |
|--------------------------| | (đã có, P1/P4) |
| id uuid pk | |--------------------------|
| owner_role / owner_ref | | revision.id, seq, trip |
| status 'draft' | | | snapshot.id, model, role |
| 'submitted' | 'expired'| +--------------------------+
+--------------------------+
Partial unique index:
create unique index submission_one_live on submission (scenario_id)
where state in ('pending','confirmed','unknown')
-- một hàng "sống" mỗi scenario; 'failed' không tính, cho phép hàng retry mới.
-- pg store bắt 23505 trên đúng tên index này -> 'already' (đổi tên là đổi cả hai).
Index đọc: submission_scenario_idx (scenario_id, created_at desc) — latestSubmission.
RLS: bật trên submission, KHÔNG có policy -> chỉ BFF nối bằng DATABASE_URL.