Skip to main content

User Request Flow

A user request asks to change or cancel a passenger's booked flight. The admin quotes it as a list of fees — a seat, a baggage or another charge. The requester then confirms the quote, choosing how much of each fee the company pays, or declines it. If the requester keeps a part for themselves, they pay it first; the company's approvers review the rest; and finally the admin applies each fee to the booking.

This page covers the status flow and the UserRequest object. Each operation has its own page under APIs, and the errors are under Errors.

What changed​

  • The quote is a list of fees. process_user_request takes fees instead of amount; amount is now their sum. Fees can't change once issued.
  • The split is per fee. confirm_user_request takes the company's share of each fee (fees: [{ id, corporate_amount }]) instead of one corporate_amount.
  • The requester pays their part in the app. A new status, AWAITING_PERSONAL_PAYMENT, and a new operation, begin_personal_payment, which is paid from the requester's wallet through the store checkout — see How the requester pays.
  • Fees are applied one by one. add_passenger_fee takes user_request_fee_id with user_request_id, and uses the fee's own data and price — no more sending a price of 0.
  • Payment state moved. extra_purchase_id is removed from UserRequest. Each fee has paid and extra_purchase_ids; the request has paid_personal_amount.
  • claimed on admin_user_requests now means "every fee paid".

Status flow​

The requester acts up to three times: to create the request, to confirm or decline the quote, and to pay their part. Status values come back upper case, as shown.

An admin can still resolve or reject without quoting; a quote is what triggers the requester's steps.

StatusMeaningWho acts next
PENDINGCreated, no admin yetAdmin picks it up; requester may cancel
IN_CHARGEAn admin is working on itAdmin quotes, resolves or rejects; requester may cancel
ISSUEDAdmin quoted the fees; waiting for the requesterRequester confirms or declines
AWAITING_PERSONAL_PAYMENT newConfirmed, and the requester keeps a part; waiting for them to pay itRequester pays — see How the requester pays
CONFIRMEDRequester accepted, and their part (if any) is paid; waiting for approversCompany approvers, level by level; every level must approve
RESOLVEDDoneAdmin applies each fee with add_passenger_fee
REJECTEDRejected by the admin or by an approverNobody (final)
CANCELEDRequester withdrew before a quoteNobody (final)
DECLINEDRequester turned the quote downNobody (final)

The requester pays before approval. Approvers only see requests whose personal part is already paid. A request waiting in AWAITING_PERSONAL_PAYMENT can't be cancelled or declined, and the admin can't process it.

CONFIRMED can skip straight to RESOLVED. When the company pays nothing, or the company's policy needs no approval, the request resolves right after it reaches CONFIRMED. Mutations report the status they set, so refetch the request before rendering its status.

Fees and the split​

Each fee is one thing the passenger gets — a seat, a baggage or another charge — on one leg, priced in the request's currency. The requester decides, fee by fee, how much the company pays.

AmountWhere it livesHow it's worked out
amountRequestSum of every fee's price, set when the admin issues the quote
corporate_amountEach feeChosen by the requester at confirm, from 0 to the fee's price
corporate_amountRequestSum of the fees' corporate_amount
personal_amountRequestamount - corporate_amount: what the requester pays

The two parts are paid at different times:

PartPaid byWhenThrough
personal_amountRequester, from their walletAfter confirming, before approvalHow the requester pays
Each fee's corporate_amountCompanyWhen the admin applies that feeadd_passenger_fee, one call per fee

How the requester pays​

The requester pays personal_amount from their wallet, through the store checkout. FE starts the payment with two calls — begin_personal_payment on the flight service, then book_extra_purchase on the store service — and finishes it with the store's Payment Flow.

#CallServiceWhat it does
1begin_personal_paymentFlightReturns the extra purchase to pay: amount is personal_amount in VND, status is PAYING.
2book_extra_purchase(input: { flight: { id: "<extra purchase id>" } })StorePuts that extra purchase in the requester's cart as a shopping item. Only the requester can book it.
3The rest of the Payment FlowStorebegin_payment_checkout_session over the cart, corporate payment left off, then pay_with_wallet.

Steps 1–2 are step 3 of the Payment Flow — "create shopping_item" — so clear the cart and cancel any open checkout session first, as that page describes.

FE calls nothing after paying. The store tells the flight service, and the request moves to CONFIRMED on success; on failure it stays AWAITING_PERSONAL_PAYMENT, and FE starts again from step 1.

Each attempt is its own extra purchase, carrying the request's user_request_id and personal: true; there's at most one PAYING at a time. When the store reports a payment, it says how it was paid, and the flight service keeps that on the extra purchase as metadata.payment_method — wallet, corporate_wallet, bank_transfer, momo, p2c2p, vnpay or stripe.

The UserRequest object​

All amounts are in the request's currency. user_requests and admin_user_requests both return every field below.

FieldTypeMeaningWhen it's set
codeStringReadable reference, FUR + 8 digits, unique. Use it in the UI and for support.On create. null on requests created before codes existed.
statusUserRequestStatusAdds AWAITING_PERSONAL_PAYMENT.See Status flow.
amountDecimalSum of the fees' price.When the admin issues the quote.
fees new[UserRequestFee]The quoted fees.When the admin issues the quote. Empty before that.
corporate_amountDecimalWhat the company pays: sum of the fees' corporate_amount.At confirm. null before that.
personal_amountDecimalamount - corporate_amount: what the requester pays. Computed, never sent.Equals amount until confirm. null when there is no amount.
paid_personal_amount newDecimalHow much of personal_amount the requester has paid.0 when issued; equals personal_amount once paid.
extra_purchase_id removedStringReplaced by each fee's paid and extra_purchase_ids.Selecting it is now a query error.

The UserRequestFee object​

FieldTypeMeaning
idStringIdentifies the fee. Send it to confirm_user_request and add_passenger_fee.
fee_typeFeeTypeBAGGAGE, SEAT or OTHER.
legFlightLegDEPARTURE or RETURN.
baggageBaggageThe baggage, when fee_type is BAGGAGE.
seatSeatThe seat, when fee_type is SEAT.
other_feeOtherFeeThe charge, when fee_type is OTHER.
currencyStringThe request's currency; price and corporate_amount are in it.
priceDecimalThe fee's full price.
corporate_amountDecimalThe company's share, chosen at confirm. The requester pays price - corporate_amount. null before confirm.
paidBooleantrue once add_passenger_fee has applied the fee and charged the company's share.
extra_purchase_ids[String]The company's payment for this fee, from add_passenger_fee.

Endpoint and authentication​

Every operation in this section is a GraphQL query or mutation on the flight service's /graphql/v1 endpoint. Field and argument names are snake_case, and enum values are upper case.

HeaderRequiredDescription
AuthorizationYesThe user's session token. The flight service passes it unchanged to the store service to load the caller's profile.
OperationsWho can call
user_requests, confirm_user_request, begin_personal_payment, decline_user_request, cancel_user_request, booking_request_progressAny signed-in user; the four mutations only on requests the caller created
admin_user_requests, process_user_request, add_passenger_feeUsers with the ADMIN role

A missing or invalid token returns UNAUTHENTICATED, and the wrong role returns UNAUTHORIZED — see Middleware Errors.

APIs​

OperationKindWhoWhat it's for
user_requestsQueryRequesterList the caller's own requests
confirm_user_requestMutationRequesterAccept the quote and choose the company's share of each fee
begin_personal_payment newMutationRequesterStart paying personal_amount
decline_user_requestMutationRequesterTurn the quote down
cancel_user_requestMutationRequesterWithdraw a request before it is quoted
admin_user_requestsQueryAdminList requests, search by code, filter by claimed
process_user_requestMutationAdminPick up, quote, resolve or reject a request
add_passenger_feeMutationAdminApply one fee of a resolved request to the passenger
booking_request_progressQueryAny signed-in userShow who still has to approve a CONFIRMED request

Breaking changes and rollout​

This is on staging only; prod still runs the old flow. Ship the FE changes with the backend release, not before. begin_personal_payment also needs the matching store service release.

ChangeWhat breaksWhat to do
process_user_request takes fees; amount removed from its inputSending amount fails the requestSend the fees; amount is worked out from them
confirm_user_request takes fees; corporate_amount removed from its inputSending corporate_amount fails the requestSend each fee's corporate_amount
New status AWAITING_PERSONAL_PAYMENTUnknown status in labels and filtersShow it as "waiting for your payment", with a pay button
extra_purchase_id removed from UserRequestSelecting it fails the whole requestUse each fee's paid
add_passenger_fee needs user_request_fee_id with user_request_idApplying a request without it returns user_request_fee_id_requiredApply each fee with its own call
claimed means every fee is paidThe admin list keeps a request until all its fees are appliedNothing, if the list always sends claimed: false
personal_pay removed from UserRequest and ProcessUserRequestInputSelecting or sending it fails the whole requestDrop it; show corporate_amount and personal_amount instead
AdminUserRequestsSortInput removedA query variable declared with that type failsDeclare it as UserRequestsSortInput
ISSUED no longer means "waiting for approvers"Status labels read wrongRelabel it as "waiting for your confirmation"
Requests quoted before fees existed

They have an amount but no fees. They can still be confirmed — send no fees, and the company pays the whole amount — but add_passenger_fee can't apply them, because it needs a fee id.

Old ISSUED requests

Requests already waiting for approvers before the release are stored as ISSUED. Until the backend moves them to CONFIRMED, the FE will show them as awaiting the requester, with a confirm button that shouldn't be there.

Still to document​

  • Store payment methods other than pay_with_wallet for the personal part. The flight service records any of them, but only the wallet path is described here, and no method has been run end to end through the store yet.
  • The api-gateway base URL and path the FE uses to reach the flight service's /graphql/v1.
  • create_user_request — its input and validation rules have no page yet. Its response includes code.
  • Matching Bruno requests under api-tests/backend/flight-service/. Most of these operations change data, and add_passenger_fee and the personal payment move money, so they need a dedicated staging account and booking before they can run safely.