Chuyển tới nội dung chính

Car Rental Service

Endpoints​

EndpointDomainAuth
POST /graphql/v1Vendor ManagementAuthorization
GET /api/upHealthNone

Authentication​

SchemeHeaderApplies toOn failure
User tokenAuthorization: <user-token>POST /graphql/v1401 Unauthorized
None—/api/up—
ghi chú

The user token is resolved against the GraphQL profile service to verify vendor permissions and user identity.


Vendor Management​

createVendor mutation​

Endpoint: POST /graphql/v1 · Auth: Authorization header — user role ADMIN required.

Creates a new vendor record. The resolver first resolves owner_email to a user_id via the profile service, then guards against duplicate vendors before inserting.

Input (CreateVendorInput)​

FieldTypeRequiredDescription
nameStringYesVendor company name.
addressStringYesFull business address.
emailStringYesVendor contact email.
phone_numberStringYesPrimary contact phone number.
owner_emailStringYesEmail of the platform user who will own this vendor account. Resolved to a user_id via the profile service.

Example​

Mutation
mutation CreateVendor($input: CreateVendorInput!) {
createVendor(input: $input) {
id
name
address
email
phoneNumber
}
}
Variables
{
"input": {
"name": "ABC Car Rental",
"address": "123 Nguyen Hue, District 1, HCMC",
"email": "contact@abccar.vn",
"phone_number": "84912345678",
"owner_email": "owner@example.com"
}
}
Success response
{
"data": {
"createVendor": {
"id": "VEND-001",
"name": "ABC Car Rental",
"address": "123 Nguyen Hue, District 1, HCMC",
"email": "contact@abccar.vn",
"phoneNumber": "84912345678"
}
}
}

Errors​

Error messageCause
not_found_user_idowner_email does not match any user in the profile service.
existed_user_idA vendor already exists for the resolved user_id.
Ecto changesetInput validation failure (e.g. missing required field).
Error response
{
"data": { "createVendor": null },
"errors": [
{
"message": "not_found_user_id",
"locations": [{ "line": 2, "column": 3 }],
"path": ["createVendor"]
}
]
}

updateVendor mutation​

Endpoint: POST /graphql/v1 · Auth: Authorization header — user role ADMIN required.

Updates an existing vendor's fields. Only fields provided in the input are applied; nil values are ignored.

Input (UpdateVendorInput)​

FieldTypeRequiredDescription
idStringYesID of the vendor to update.
nameStringNoNew vendor company name.
addressStringNoNew full business address.
emailStringNoNew vendor contact email.
phone_numberStringNoNew primary contact phone number.

Example​

Mutation
mutation UpdateVendor($input: UpdateVendorInput!) {
updateVendor(input: $input) {
id
name
address
email
phoneNumber
}
}
Variables
{
"input": {
"id": "VEND-001",
"name": "ABC Car Rental Updated",
"address": "456 Le Loi, District 1, HCMC"
}
}
Success response
{
"data": {
"updateVendor": {
"id": "VEND-001",
"name": "ABC Car Rental Updated",
"address": "456 Le Loi, District 1, HCMC",
"email": "contact@abccar.vn",
"phoneNumber": "84912345678"
}
}
}

Errors​

Error messageCause
not_valid_vendorNo vendor found for the given id.
Ecto changesetInput validation failure.

deleteVendor mutation​

Endpoint: POST /graphql/v1 · Auth: Authorization header — user role ADMIN required.

Deletes a vendor by ID. Returns true on success.

Input (VendorInput)​

FieldTypeRequiredDescription
idStringYesID of the vendor to delete.

Example​

Mutation
mutation DeleteVendor($input: VendorInput!) {
deleteVendor(input: $input)
}
Variables
{
"input": {
"id": "VEND-001"
}
}
Success response
{
"data": {
"deleteVendor": true
}
}

Errors​

Error messageCause
not_valid_vendorNo vendor found for the given id.

createVendorStaff mutation​

Endpoint: POST /graphql/v1 · Auth: Authorization header — user role ADMIN required.

Assigns a platform user as a staff member of a vendor. The resolver validates that both the vendor and the user exist, then guards against duplicate staff assignments before inserting.

Input (CreateVendorStaffInput)​

FieldTypeRequiredDescription
vendor_idStringYesID of the vendor to assign the staff member to.
user_idIntegerYesPlatform user ID. Verified against the profile service before insert.
activeBooleanYesWhether the staff member is currently active.
roleVendorStaffRoleYesRole within the vendor. Enum: STAFF, ADMIN.

Example​

Mutation
mutation CreateVendorStaff($input: CreateVendorStaffInput!) {
createVendorStaff(input: $input) {
id
vendorId
userId
active
role
}
}
Variables
{
"input": {
"vendor_id": "VEND-001",
"user_id": 42,
"active": true,
"role": "STAFF"
}
}
Success response
{
"data": {
"createVendorStaff": {
"id": "VS-001",
"vendorId": "VEND-001",
"userId": 42,
"active": true,
"role": "STAFF"
}
}
}

Errors​

Error messageCause
not_valid_vendorNo vendor found for the given vendor_id.
not_existed_userNo platform user found for the given user_id.
existed_vendor_staffA staff record already exists for this vendor_id + user_id pair.
Ecto changesetInput validation failure.

deleteVendorStaff mutation​

Endpoint: POST /graphql/v1 · Auth: Authorization header — user role ADMIN required.

Removes a staff member from a vendor by staff record ID. Returns true on success.

Input (VendorStaffInput)​

FieldTypeRequiredDescription
idStringYesID of the vendor staff record to delete.

Example​

Mutation
mutation DeleteVendorStaff($input: VendorStaffInput!) {
deleteVendorStaff(input: $input)
}
Variables
{
"input": {
"id": "VS-001"
}
}
Success response
{
"data": {
"deleteVendorStaff": true
}
}

Errors​

Error messageCause
not_existed_vendor_staffNo staff record found for the given id.

createVendorVehicle mutation​

Endpoint: POST /graphql/v1 · Auth: Authorization header — user role ADMIN required.

Links a vehicle to a vendor. Runs inside a database transaction that also seeds full-day rental price rows (one per vendor region × num-days config) and one-way route price rows (one per vendor route) for the new vehicle.

Input (CreateVendorVehicleInput)​

FieldTypeRequiredDescription
vendor_idStringYesID of the vendor to assign the vehicle to.
vehicle_idStringYesID of the vehicle to link.

Example​

Mutation
mutation CreateVendorVehicle($input: CreateVendorVehicleInput!) {
createVendorVehicle(input: $input) {
id
vendorId
vehicleId
}
}
Variables
{
"input": {
"vendor_id": "VEND-001",
"vehicle_id": "VEH-007"
}
}
Success response
{
"data": {
"createVendorVehicle": {
"id": "VV-001",
"vendorId": "VEND-001",
"vehicleId": "VEH-007"
}
}
}

Errors​

Error messageCause
not_found_vendorNo vendor found for the given vendor_id.
not_found_vehicleNo vehicle found for the given vehicle_id.
vendor_vehicle_already_existsThis vendor–vehicle pair is already linked.
Ecto changesetInput validation failure.

deleteVendorVehicle mutation​

Endpoint: POST /graphql/v1 · Auth: Authorization header — user role ADMIN required.

Unlinks a vehicle from a vendor. Runs inside a database transaction that also deletes all full-day rental price rows associated with the vendor–vehicle pair. Returns true on success.

Input (VendorVehicleInput)​

FieldTypeRequiredDescription
idStringYesID of the vendor vehicle record to delete.

Example​

Mutation
mutation DeleteVendorVehicle($input: VendorVehicleInput!) {
deleteVendorVehicle(input: $input)
}
Variables
{
"input": {
"id": "VV-001"
}
}
Success response
{
"data": {
"deleteVendorVehicle": true
}
}

Errors​

Error messageCause
not_found_vendor_vehicleNo vendor vehicle record found for the given id.

Region Management​

createRegion mutation​

Endpoint: POST /graphql/v1 · Auth: Authorization header — vendor role ADMIN required.

Creates a region for the authenticated vendor and bulk-inserts its provinces. Also seeds full-day rental price rows (one per existing vendor vehicle) for the new region — all inside a single transaction. The vendor_id is taken from the authenticated vendor context, not from the input.

Input (CreateRegionInput)​

FieldTypeRequiredDescription
nameStringYesRegion display name.
statusBooleanYesWhether the region is active.
time_per_dayDecimalYesAllowed driving hours per day for this region.
distance_per_dayDecimalYesAllowed driving distance (km) per day for this region.
provinces[ProvinceInput!]!YesList of provinces belonging to this region. Must not be empty.

ProvinceInput

FieldTypeRequiredDescription
city_codeStringYesCity/province code.
country_codeStringYesISO country code (e.g. "VN").
nameStringYesProvince display name.

Example​

Mutation
mutation CreateRegion($input: CreateRegionInput!) {
createRegion(input: $input) {
id
vendorId
name
status
timePerDay
distancePerDay
provinces {
id
cityCode
countryCode
name
}
}
}
Variables
{
"input": {
"name": "Southern Region",
"status": true,
"time_per_day": "8",
"distance_per_day": "300",
"provinces": [
{ "city_code": "SGN", "country_code": "VN", "name": "Ho Chi Minh City" },
{ "city_code": "BDG", "country_code": "VN", "name": "Binh Duong" }
]
}
}
Success response
{
"data": {
"createRegion": {
"id": "REG-001",
"vendorId": "VEND-001",
"name": "Southern Region",
"status": true,
"timePerDay": "8",
"distancePerDay": "300",
"provinces": [
{ "id": "RP-001", "cityCode": "SGN", "countryCode": "VN", "name": "Ho Chi Minh City" },
{ "id": "RP-002", "cityCode": "BDG", "countryCode": "VN", "name": "Binh Duong" }
]
}
}
}

Errors​

Error messageCause
Ecto changesetInput validation failure.

updateRegion mutation​

Endpoint: POST /graphql/v1 · Auth: Authorization header — vendor role ADMIN required.

Updates region fields for the authenticated vendor. nil fields are ignored. If provinces is provided, all existing provinces for the region are deleted and replaced with the new list — all inside a single transaction.

Input (UpdateRegionInput)​

FieldTypeRequiredDescription
idStringYesID of the region to update.
nameStringNoNew region display name.
statusBooleanNoNew active status.
time_per_dayDecimalNoNew allowed driving hours per day.
distance_per_dayDecimalNoNew allowed driving distance per day.
provinces[ProvinceInput!]NoFull replacement list of provinces. When provided, all existing provinces are deleted and reinserted.

Example​

Mutation
mutation UpdateRegion($input: UpdateRegionInput!) {
updateRegion(input: $input) {
id
name
status
timePerDay
distancePerDay
provinces {
cityCode
countryCode
name
}
}
}
Variables
{
"input": {
"id": "REG-001",
"name": "Southern Region (Updated)",
"provinces": [
{ "city_code": "SGN", "country_code": "VN", "name": "Ho Chi Minh City" },
{ "city_code": "DNG", "country_code": "VN", "name": "Da Nang" }
]
}
}
Success response
{
"data": {
"updateRegion": {
"id": "REG-001",
"name": "Southern Region (Updated)",
"status": true,
"timePerDay": "8",
"distancePerDay": "300",
"provinces": [
{ "cityCode": "SGN", "countryCode": "VN", "name": "Ho Chi Minh City" },
{ "cityCode": "DNG", "countryCode": "VN", "name": "Da Nang" }
]
}
}
}

Errors​

Error messageCause
not_found_regionNo region found for the given id under the authenticated vendor.
Ecto changesetInput validation failure.

deleteRegion mutation​

Endpoint: POST /graphql/v1 · Auth: Authorization header — vendor role ADMIN required.

Deletes a region belonging to the authenticated vendor. Runs a transaction that cascades to delete all associated provinces and full-day rental prices before removing the region itself. Returns true on success.

Input (RegionInput)​

FieldTypeRequiredDescription
idStringYesID of the region to delete.

Example​

Mutation
mutation DeleteRegion($input: RegionInput!) {
deleteRegion(input: $input)
}
Variables
{
"input": {
"id": "REG-001"
}
}
Success response
{
"data": {
"deleteRegion": true
}
}

Errors​

Error messageCause
not_found_regionNo region found for the given id under the authenticated vendor.

regions query​

Endpoint: POST /graphql/v1 · Auth: Authorization header — vendor role ADMIN required.

Returns a cursor-paginated list of regions belonging to the authenticated vendor, ordered by inserted_at ascending.

Input (ListRegionsInput)​

FieldTypeRequiredDescription
afterStringNoCursor for pagination (opaque string).
limitIntegerNoNumber of records to return. Default: 10.

Example​

Query
query Regions($input: ListRegionsInput!) {
regions(input: $input) {
entries {
id
name
status
timePerDay
distancePerDay
provinces {
cityCode
countryCode
name
}
}
metadata {
after
totalCount
}
}
}
Variables
{
"input": { "limit": 10 }
}
Success response
{
"data": {
"regions": {
"entries": [
{
"id": "REG-001",
"name": "Southern Region",
"status": true,
"timePerDay": "8",
"distancePerDay": "300",
"provinces": [
{ "cityCode": "SGN", "countryCode": "VN", "name": "Ho Chi Minh City" }
]
}
],
"metadata": { "after": "cursor_opaque_string", "totalCount": 1 }
}
}
}

region query​

Endpoint: POST /graphql/v1 · Auth: Authorization header — corporate user required.

Returns a single region by ID.

Input (RegionInput)​

FieldTypeRequiredDescription
idStringYesID of the region to fetch.

Example​

Query
query Region($input: RegionInput!) {
region(input: $input) {
id
vendorId
name
status
timePerDay
distancePerDay
provinces {
cityCode
countryCode
name
}
}
}
Variables
{
"input": { "id": "REG-001" }
}
Success response
{
"data": {
"region": {
"id": "REG-001",
"vendorId": "VEND-001",
"name": "Southern Region",
"status": true,
"timePerDay": "8",
"distancePerDay": "300",
"provinces": [
{ "cityCode": "SGN", "countryCode": "VN", "name": "Ho Chi Minh City" }
]
}
}
}

Full-Day Rental Prices​

updateFullDayRentalPrice mutation​

Endpoint: POST /graphql/v1 · Auth: Authorization header — vendor role ADMIN required.

Updates pricing fields for an existing full-day rental price record owned by the authenticated vendor. Only provided (non-nil) fields are applied.

Full-day rental price records are created automatically when a vendor vehicle or a region is created — they are never created manually.

Input (UpdateFullDayRentalPriceInput)​

FieldTypeRequiredDescription
idIDYesID of the full-day rental price record to update.
overtime_priceDecimalNoPrice charged per hour of overtime beyond time_per_day.
over_distance_priceDecimalNoPrice charged per km beyond distance_per_day.
currencyStringNoCurrency code (e.g. "VND").
num_days_config[Decimal]NoOrdered list of day thresholds that define the pricing tiers (e.g. [1, 3, 7]).
daily_price_tiers[DailyPriceTierInput]NoPricing tiers. Each tier maps a num_days threshold to a price and currency.

DailyPriceTierInput

FieldTypeRequiredDescription
num_daysDecimalNoDay threshold for this tier.
priceDecimalNoPrice per day at this tier.
currencyStringNoCurrency code for this tier (e.g. "VND").

Example​

Mutation
mutation UpdateFullDayRentalPrice($input: UpdateFullDayRentalPriceInput!) {
updateFullDayRentalPrice(input: $input) {
id
vendorId
regionId
vehicleId
overtimePrice
overDistancePrice
currency
numDaysConfig
dailyPriceTiers {
numDays
price
currency
}
}
}
Variables
{
"input": {
"id": "FDRP-001",
"overtime_price": "150000",
"over_distance_price": "5000",
"currency": "VND",
"num_days_config": ["1", "3", "7"],
"daily_price_tiers": [
{ "num_days": "1", "price": "800000", "currency": "VND" },
{ "num_days": "3", "price": "750000", "currency": "VND" },
{ "num_days": "7", "price": "700000", "currency": "VND" }
]
}
}
Success response
{
"data": {
"updateFullDayRentalPrice": {
"id": "FDRP-001",
"vendorId": "VEND-001",
"regionId": "REG-001",
"vehicleId": "VEH-007",
"overtimePrice": "150000",
"overDistancePrice": "5000",
"currency": "VND",
"numDaysConfig": ["1", "3", "7"],
"dailyPriceTiers": [
{ "numDays": "1", "price": "800000", "currency": "VND" },
{ "numDays": "3", "price": "750000", "currency": "VND" },
{ "numDays": "7", "price": "700000", "currency": "VND" }
]
}
}
}

Errors​

Error messageCause
not_found_full_day_rental_priceNo record found for the given id under the authenticated vendor.
Ecto changesetInput validation failure.

fullDayRentalAmount query​

Endpoint: POST /graphql/v1 · Auth: Authorization header — corporate user required.

Calculates the total rental amount for a full-day booking given pickup/dropoff locations, times, vehicle, and vendor. The resolver resolves the active region from the pickup and dropoff city codes, looks up the matching price record, computes the number of rental days (rounded up to the next whole day, minimum 1), selects the best matching price tier, and returns the total amount.

Tier selection logic​

The resolver picks the tier whose num_days is the largest value ≤ the computed rental days. If no tier qualifies, the last tier in the list is used as a fallback.

Input (FullDayRentalAmountInput)​

FieldTypeRequiredDescription
pickup_locationLocationInputYesPickup city — city_code and country_code used to resolve the region.
dropoff_locationLocationInputYesDropoff city — must share an active region with the pickup location.
pickup_timeDateTimeYesRental start time (ISO 8601).
dropoff_timeDateTimeYesRental end time (ISO 8601). Used with pickup_time to compute rental days.
vehicle_idStringYesID of the vehicle being rented.
vendor_idStringYesID of the vendor providing the vehicle.

Example​

Query
query FullDayRentalAmount($input: FullDayRentalAmountInput!) {
fullDayRentalAmount(input: $input) {
amount
currency
}
}
Variables
{
"input": {
"pickup_location": { "city_code": "SGN", "country_code": "VN" },
"dropoff_location": { "city_code": "BDG", "country_code": "VN" },
"pickup_time": "2026-07-01T08:00:00Z",
"dropoff_time": "2026-07-03T08:00:00Z",
"vehicle_id": "VEH-007",
"vendor_id": "VEND-001"
}
}
Success response
{
"data": {
"fullDayRentalAmount": {
"amount": "1500000",
"currency": "VND"
}
}
}

Errors​

Error messageCause
not_in_same_regionPickup and dropoff locations do not share an active region for the given vendor.
full_day_rental_price_not_foundNo price record exists for the resolved region + vehicle combination.
no_price_tier_configuredThe price record has no daily_price_tiers entries.
price_not_configured_for_tierThe matched tier has a nil price — pricing has not been set up for this tier.

Booking Requests​

rejectBookingRequest mutation​

cẩn thận

This mutation is currently disabled on the frontend and is not available to end users.

Endpoint: POST /graphql/v1 · Auth: Authorization header — vendor role ADMIN required.

Rejects a booking request that belongs to the authenticated vendor. Only requests in REQUESTED status can be rejected. On success the resolver atomically:

  1. Sets the booking request status to REJECTED.
  2. Sets the linked booking status to CANCELLED.
  3. Calls the TripBase product service to update the external trip product status to REJECTED.
  4. Sends a rejection notification to the passenger via the notification center.

Returns the updated BookingRequest object.

Input (BookingRequestInput)​

FieldTypeRequiredDescription
idStringYesID of the booking request to reject.

Example​

Mutation
mutation RejectBookingRequest($input: BookingRequestInput!) {
rejectBookingRequest(input: $input) {
id
vendorId
corporateId
status
pickUpTime
dropOffTime
note
}
}
Variables
{
"input": {
"id": "BR-001"
}
}
Success response
{
"data": {
"rejectBookingRequest": {
"id": "BR-001",
"vendorId": "VEND-001",
"corporateId": "CORP-001",
"status": "REJECTED",
"pickUpTime": "2026-07-01T08:00:00Z",
"dropOffTime": "2026-07-03T08:00:00Z",
"note": null
}
}
}

Errors​

Error messageCause
not_found_booking_requestNo booking request found for the given id under the authenticated vendor.
cannot_reject_booking_requestThe booking request exists but is not in REQUESTED status.
not_found_bookingThe linked booking record could not be found.

createBookingRequest mutation​

Endpoint: POST /graphql/v1 · Auth: Authorization header — corporate user required.

Creates a booking request. The caller must be the trip owner (verified against the TripBase service). Supports two booking types: FULL_DAY and ONE_WAY_ROUTE.

On success the resolver atomically:

  1. Validates the vendor exists.
  2. For FULL_DAY: resolves the active region from pickup/dropoff city codes.
  3. Calculates the total price across all requested vehicles and quantities.
  4. Creates a Booking record with status PENDING.
  5. Creates the BookingRequest record with status REQUESTED.
  6. Registers a trip product with TripBase.
  7. Sends a notification to the passenger.
  8. Records a creation history entry.

Input (CreateBookingRequestInput)​

FieldTypeRequiredDescription
vendor_idStringYesID of the vendor to request from.
corporate_idStringYesID of the corporate making the request.
trip_idStringYesTripBase trip ID. The caller must be the trip owner.
vehicles[VehicleQuantityInput!]!YesVehicles and quantities to request.
typeBookingTypeYesFULL_DAY or ONE_WAY_ROUTE.
passengers[PassengerInput!]!YesList of passengers.
pick_up_locationPlaceInputYesPickup address and coordinates.
pick_up_timeDateTimeYesPickup time (ISO 8601).
drop_off_locationPlaceInputYesDrop-off address and coordinates.
drop_off_timeDateTimeFULL_DAY onlyDrop-off time. Required when type is FULL_DAY.
route_idStringONE_WAY_ROUTE onlyRoute ID. Required when type is ONE_WAY_ROUTE.
begin_locationLocationInputYesBegin location of the trip itinerary that contains this booking request (city_code, country_code).
end_locationLocationInputYesEnd location of the trip itinerary that contains this booking request (city_code, country_code).
noteStringNoOptional note for the vendor.

VehicleQuantityInput · PlaceInput · PassengerInput

TypeFields
VehicleQuantityInputid (String, required), quantity (Integer, required)
PlaceInputaddress (String, required), location (LocationInput, required)
PassengerInputuser_id (Integer), full_name (String), email (String), phone_number (String), is_contact_person (Boolean)

Example​

Mutation
mutation CreateBookingRequest($input: CreateBookingRequestInput!) {
createBookingRequest(input: $input) {
id
vendorId
corporateId
tripId
status
type
pickUpTime
dropOffTime
code
note
}
}
Variables
{
"input": {
"vendor_id": "VEND-001",
"corporate_id": "CORP-001",
"trip_id": "TRIP-001",
"type": "FULL_DAY",
"vehicles": [{ "id": "VEH-007", "quantity": 1 }],
"passengers": [{ "user_id": 42, "full_name": "Nguyen Van A", "is_contact_person": true }],
"pick_up_location": { "address": "123 Nguyen Hue, HCMC", "location": { "city_code": "SGN", "country_code": "VN" } },
"drop_off_location": { "address": "456 Le Loi, HCMC", "location": { "city_code": "BDG", "country_code": "VN" } },
"pick_up_time": "2026-07-01T08:00:00Z",
"drop_off_time": "2026-07-03T08:00:00Z",
"begin_location": { "city_code": "SGN", "country_code": "VN" },
"end_location": { "city_code": "BDG", "country_code": "VN" }
}
}
Success response
{
"data": {
"createBookingRequest": {
"id": "BR-001",
"vendorId": "VEND-001",
"corporateId": "CORP-001",
"tripId": "TRIP-001",
"status": "REQUESTED",
"type": "FULL_DAY",
"pickUpTime": "2026-07-01T08:00:00Z",
"dropOffTime": "2026-07-03T08:00:00Z",
"code": "CORP-A1B2C3",
"note": null
}
}
}

Errors​

Error messageCause
not_trip_ownerThe authenticated user is not the owner of the given trip in TripBase.
not_valid_vendor_idNo vendor found for the given vendor_id.
not_found_regionFULL_DAY only — pickup/dropoff locations do not share an active region.
Ecto changesetInput validation failure.

updateBookingRequest mutation​

Endpoint: POST /graphql/v1 · Auth: Authorization header — corporate user required.

Updates an existing booking request. Only requests in REQUESTED or COMPLETED status can be updated. Recalculates the booking price after applying the new vehicles/times. If the linked booking was ACCEPTED, it is reverted to PENDING; if the booking request was COMPLETED, it is reverted to REQUESTED.

On success the resolver atomically:

  1. Guards status (REQUESTED or COMPLETED only) and loads the linked booking.
  2. Validates pickup time is not in the past.
  3. For ONE_WAY_ROUTE: validates route_id is provided and exists.
  4. For FULL_DAY: validates drop_off_time is provided and resolves the active region.
  5. Recalculates the total price across vehicles.
  6. Updates the linked Booking and the BookingRequest.
  7. Records history entries and syncs to TripBase.

Input (UpdateBookingRequestInput)​

FieldTypeRequiredDescription
idStringYesID of the booking request to update.
vehicles[VehicleQuantityInput!]!YesReplacement vehicle list with quantities.
pick_up_locationPlaceInputYesNew pickup address and coordinates.
pick_up_timeDateTimeYesNew pickup time. Must not be in the past.
drop_off_locationPlaceInputYesNew drop-off address and coordinates.
drop_off_timeDateTimeFULL_DAY onlyNew drop-off time. Required for FULL_DAY bookings.
route_idStringONE_WAY_ROUTE onlyRoute ID. Required for ONE_WAY_ROUTE bookings.
noteStringNoUpdated note.

Example​

Mutation
mutation UpdateBookingRequest($input: UpdateBookingRequestInput!) {
updateBookingRequest(input: $input) {
id
status
pickUpTime
dropOffTime
note
}
}
Variables
{
"input": {
"id": "BR-001",
"vehicles": [{ "id": "VEH-007", "quantity": 2 }],
"pick_up_location": { "address": "123 Nguyen Hue, HCMC", "location": { "city_code": "SGN", "country_code": "VN" } },
"drop_off_location": { "address": "456 Le Loi, HCMC", "location": { "city_code": "BDG", "country_code": "VN" } },
"pick_up_time": "2026-07-02T08:00:00Z",
"drop_off_time": "2026-07-04T08:00:00Z",
"note": "Please bring child seats"
}
}
Success response
{
"data": {
"updateBookingRequest": {
"id": "BR-001",
"status": "REQUESTED",
"pickUpTime": "2026-07-02T08:00:00Z",
"dropOffTime": "2026-07-04T08:00:00Z",
"note": "Please bring child seats"
}
}
}

Errors​

Error messageCause
not_found_booking_requestNo booking request found for the given id.
not_allow_update_booking_requestStatus is not REQUESTED or COMPLETED.
not_found_bookingThe linked booking record could not be found.
invalid_pick_up_timepick_up_time is in the past.
route_id_requiredONE_WAY_ROUTE booking but route_id was not provided.
not_found_routeThe given route_id does not exist.
drop_off_time_requiredFULL_DAY booking but drop_off_time was not provided.
pick_up_time_must_be_before_drop_off_timepick_up_time is not earlier than drop_off_time.
not_found_regionFULL_DAY only — pickup/dropoff locations do not share an active region.

cancelBookingRequest mutation​

Endpoint: POST /graphql/v1 · Auth: Authorization header — corporate user required.

Cancels a booking request. Only requests in REQUESTED or ACCEPTED status can be cancelled. The authenticated user must be the trip owner.

On success the resolver atomically:

  1. Verifies the booking request belongs to the authenticated user with a cancellable status.
  2. Sets the booking request to CANCELLED and records history.
  3. Sets the linked booking to CANCELLED.
  4. Updates the trip product status in TripBase to CANCELED.
  5. Syncs the updated booking request to TripBase.

Input (BookingRequestInput)​

FieldTypeRequiredDescription
idStringYesID of the booking request to cancel.

Example​

Mutation
mutation CancelBookingRequest($input: BookingRequestInput!) {
cancelBookingRequest(input: $input) {
id
status
}
}
Variables
{
"input": { "id": "BR-001" }
}
Success response
{
"data": {
"cancelBookingRequest": {
"id": "BR-001",
"status": "CANCELLED"
}
}
}

Errors​

Error messageCause
not_found_booking_requestNo booking request found for the given id.
not_trip_ownerThe authenticated user is not the trip owner in TripBase.
unauthorizedThe booking request belongs to a different user.
not_allow_cancel_booking_requestStatus is not REQUESTED or ACCEPTED.
not_found_bookingThe linked booking record could not be found.

bookingRequests query​

Endpoint: POST /graphql/v1 · Auth: Authorization header — vendor role ADMIN required.

Returns a cursor-paginated list of booking requests for the authenticated vendor, ordered by inserted_at descending. Includes total count.

Input (ListBookingRequestsInput)​

FieldTypeRequiredDescription
afterStringNoCursor for pagination.
limitIntegerNoRecords to return. Default: 20.
filter.corporate_idStringNoFilter by corporate.
filter.textStringNoCase-insensitive search across booking code, contact person full_name, email, phone_number.

Example​

Query
query BookingRequests($input: ListBookingRequestsInput!) {
bookingRequests(input: $input) {
entries {
id
status
type
code
pickUpTime
dropOffTime
contactPerson { fullName email phoneNumber }
}
metadata { after totalCount }
}
}
Variables
{
"input": { "limit": 20, "filter": { "text": "Nguyen Van A" } }
}

corporateBookingRequests query​

Endpoint: POST /graphql/v1 · Auth: Authorization header — corporate user required.

Returns a cursor-paginated list of booking requests for the authenticated corporate. Has two modes:

  • Trip mode — when trip_id + begin_location + end_location are all provided: filters by exact trip and route match, scoped to REQUESTED or COMPLETED status. No corporate role check.
  • List mode — when trip_id is absent: requires corporate role OWNER or ADMIN. Returns all booking requests for the corporate with optional text search. Includes total count.

Input (ListBookingRequestsInput)​

FieldTypeRequiredDescription
afterStringNoCursor for pagination.
limitIntegerNoRecords to return. Default: 20.
filter.trip_idStringTrip modeWhen combined with locations, activates trip mode.
filter.begin_locationLocationInputTrip modeOrigin city. Required in trip mode.
filter.end_locationLocationInputTrip modeDestination city. Required in trip mode.
filter.textStringNo (list mode)Searches booking code, contact person full_name, email, phone_number.

Example​

Query
query CorporateBookingRequests($input: ListBookingRequestsInput!) {
corporateBookingRequests(input: $input) {
entries {
id
status
type
code
pickUpTime
dropOffTime
}
metadata { after totalCount }
}
}
Variables (list mode)
{
"input": { "limit": 20, "filter": { "text": "BR-" } }
}
Variables (trip mode)
{
"input": {
"limit": 20,
"filter": {
"trip_id": "TRIP-001",
"begin_location": { "city_code": "SGN", "country_code": "VN" },
"end_location": { "city_code": "BDG", "country_code": "VN" }
}
}
}

Errors​

Error messageCause
unauthorizedList mode — corporate user lacks OWNER or ADMIN role.
required_corporateNo corporate context found for the authenticated user.

bookingRequest query​

Endpoint: POST /graphql/v1 · Auth: Authorization header — corporate user required.

Fetches a single booking request by ID.

ghi chú

The status field is computed: when the raw status is COMPLETED, the resolver returns the linked booking's current status instead. The region field is only populated for FULL_DAY booking requests. The route field is only populated for ONE_WAY_ROUTE booking requests.

Input (BookingRequestInput)​

FieldTypeRequiredDescription
idStringYesID of the booking request to fetch.

Example​

Query
query BookingRequest($input: BookingRequestInput!) {
bookingRequest(input: $input) {
id
vendorId
corporateId
tripId
status
type
code
pickUpTime
dropOffTime
note
requestedAt
legalEntity
contactPerson { fullName email phoneNumber }
user { email phoneNumber }
corporate { companyName displayName logo }
region { id name }
route { id }
}
}
Variables
{
"input": { "id": "BR-001" }
}
Success response
{
"data": {
"bookingRequest": {
"id": "BR-001",
"vendorId": "VEND-001",
"corporateId": "CORP-001",
"tripId": "TRIP-001",
"status": "REQUESTED",
"type": "FULL_DAY",
"code": "CORP-A1B2C3",
"pickUpTime": "2026-07-01T08:00:00Z",
"dropOffTime": "2026-07-03T08:00:00Z",
"note": null,
"requestedAt": "2026-06-20T09:00:00Z",
"legalEntity": "ABC Corp Ltd",
"contactPerson": { "fullName": "Nguyen Van A", "email": "a@example.com", "phoneNumber": "84912345678" },
"user": { "email": "a@example.com", "phoneNumber": "84912345678" },
"corporate": { "companyName": "ABC Corp", "displayName": "ABC", "logo": null },
"region": { "id": "REG-001", "name": "Southern Region" },
"route": null
}
}
}

Bookings​

createBooking mutation​

Endpoint: POST /graphql/v1 · Auth: Authorization header — vendor role ADMIN required.

Accepts a booking request by assigning drivers to vehicles. The linked booking request must be in REQUESTED status and owned by the authenticated vendor. Pickup time must be in the future.

On success the resolver atomically:

  1. Sets the booking request to COMPLETED.
  2. Sets the booking to ACCEPTED and stores vehicles_drivers in metadata.
  3. Syncs the updated booking to TripBase.
  4. Notifies the passenger.

Input (CreateBookingInput)​

FieldTypeRequiredDescription
booking_request_idStringYesID of the booking request to accept.
vehicles_drivers[VehicleDriverInput!]YesDriver assignments — one entry per vehicle unit. Count per vehicle_id must match the requested quantity.

VehicleDriverInput

FieldTypeRequiredDescription
vehicle_idStringYesVehicle ID (must be in the booking request).
driver_nameStringYesDriver full name.
driver_phone_numberStringYesDriver contact phone number.
vehicle_plate_numberStringYesVehicle plate number.

Example​

Mutation
mutation CreateBooking($input: CreateBookingInput!) {
createBooking(input: $input) {
id
status
price
pickUpTime
dropOffTime
}
}
Variables
{
"input": {
"booking_request_id": "BR-001",
"vehicles_drivers": [
{
"vehicle_id": "VEH-007",
"driver_name": "Tran Van B",
"driver_phone_number": "84987654321",
"vehicle_plate_number": "51A-12345"
}
]
}
}
Success response
{
"data": {
"createBooking": {
"id": "BOOK-001",
"status": "ACCEPTED",
"price": "1600000",
"pickUpTime": "2026-07-01T08:00:00Z",
"dropOffTime": "2026-07-03T08:00:00Z"
}
}
}

Errors​

Error messageCause
not_found_booking_requestNo booking request found for the given ID under the authenticated vendor.
cannot_create_bookingThe booking request is not in REQUESTED status.
not_found_bookingThe linked booking record could not be found.
invalid_pick_up_timeThe pickup time is not in the future.
invalid_vehicles_drivers_formatvehicles_drivers is empty or not provided.
invalid_vehicle_in_vehicles_driversA vehicle_id in vehicles_drivers was not part of the original booking request.
vehicles_drivers_quantity_exceeds_requestedThe count of entries per vehicle_id does not match the requested quantity.

updateBooking mutation​

Endpoint: POST /graphql/v1 · Auth: Authorization header — vendor role ADMIN required.

Updates the driver assignments (vehicles_drivers) on an accepted booking. The linked booking request must be in COMPLETED status, the booking must be in ACCEPTED status, and the pickup time must be in the future.

On success the resolver atomically:

  1. Replaces vehicles_drivers in the booking metadata.
  2. Syncs the updated booking to TripBase.
  3. Notifies the passenger.

Input (UpdateBookingInput)​

FieldTypeRequiredDescription
idStringYesBooking ID to update.
vehicles_drivers[VehicleDriverInput!]YesNew driver assignments. Count per vehicle_id must match the requested quantity.

Example​

Mutation
mutation UpdateBooking($input: UpdateBookingInput!) {
updateBooking(input: $input) {
id
status
}
}
Variables
{
"input": {
"id": "BOOK-001",
"vehicles_drivers": [
{
"vehicle_id": "VEH-007",
"driver_name": "Le Van C",
"driver_phone_number": "84911111111",
"vehicle_plate_number": "51A-99999"
}
]
}
}
Success response
{
"data": {
"updateBooking": {
"id": "BOOK-001",
"status": "ACCEPTED"
}
}
}

Errors​

Error messageCause
not_found_booking_requestNo booking request found for this booking under the authenticated vendor.
cannot_update_booking_requestThe booking request is not in COMPLETED status.
invalid_pick_up_timeThe pickup time is not in the future.
not_found_bookingThe booking record could not be found.
cannot_update_bookingThe booking is not in ACCEPTED status.
invalid_vehicle_in_vehicles_driversA vehicle_id in vehicles_drivers was not part of the original booking request.
vehicles_drivers_quantity_exceeds_requestedThe count per vehicle_id does not match the requested quantity.

updateFinishedBooking mutation​

Endpoint: POST /graphql/v1 · Auth: Authorization header — vendor role ADMIN required.

Submits timesheet data for a completed trip. The booking must be in ACCEPTED status and the pickup time must already be in the past. Recalculates the final price from the timesheets.

Price calculation per booking type:

  • FULL_DAY: daily_price + overtime_hours × overtime_price + excess_km × over_distance_price + toll_fees per entry.
  • ONE_WAY_ROUTE: route_base_price + total_toll_fees across all entries.

Input (UpdateFinishedBookingInput)​

FieldTypeRequiredDescription
idStringYesBooking ID.
timesheets[TimesheetInput!]YesOne timesheet per vehicle driver. Must be non-empty with at least one entry each.

TimesheetInput

FieldTypeRequiredDescription
vehicle_driverVehicleDriverInputYesDriver and vehicle identification.
timesheet_entries[TimesheetEntryInput!]YesDaily trip records. Must be non-empty.

TimesheetEntryInput

FieldTypeRequiredDescription
dateDateTimeYesDate of the trip entry (used for FULL_DAY date validation).
toll_feeDecimalYesToll fees incurred on this day.
start_working_timeDateTimeNoStart of driving time. Used for overtime calculation.
end_working_timeDateTimeNoEnd of driving time. Used for overtime calculation.
start_odometerDecimalNoStarting odometer reading (km). Used for excess distance calc.
end_odometerDecimalNoEnding odometer reading (km). Used for excess distance calc.
routeStringNoRoute description for this entry.

Example​

Mutation
mutation UpdateFinishedBooking($input: UpdateFinishedBookingInput!) {
updateFinishedBooking(input: $input) {
id
status
price
}
}
Variables
{
"input": {
"id": "BOOK-001",
"timesheets": [
{
"vehicle_driver": {
"vehicle_id": "VEH-007",
"driver_name": "Tran Van B",
"driver_phone_number": "84987654321",
"vehicle_plate_number": "51A-12345"
},
"timesheet_entries": [
{
"date": "2026-07-01T01:00:00Z",
"start_working_time": "2026-07-01T01:00:00Z",
"end_working_time": "2026-07-01T10:00:00Z",
"start_odometer": "0",
"end_odometer": "280",
"toll_fee": "50000",
"route": "SGN - BDG"
}
]
}
]
}
}
Success response
{
"data": {
"updateFinishedBooking": {
"id": "BOOK-001",
"status": "ACCEPTED",
"price": "1700000"
}
}
}

Errors​

Error messageCause
not_found_booking_requestNo booking request found for this booking under the authenticated vendor.
cannot_update_booking_requestThe booking request is not in COMPLETED status.
not_found_bookingThe booking record could not be found.
cannot_update_bookingThe booking is not in ACCEPTED status.
invalid_drop_off_timeThe pickup time has not passed yet — the trip has not started.
invalid_timesheets_formattimesheets is empty, or one or more timesheets have no entries.

confirmTripCompletion mutation​

Endpoint: POST /graphql/v1 · Auth: Authorization header — vendor role ADMIN required.

Marks an accepted booking as completed by the vendor (sets status to UPDATED_BY_VENDOR). The trip must have already passed its end time. If no timesheets are present in metadata for a ONE_WAY_ROUTE booking, stub timesheets are auto-generated from the existing vehicles_drivers. For FULL_DAY bookings, timesheets must already exist and cover every date in the pickup–dropoff range.

On success the resolver atomically:

  1. Validates timesheets (full_day) or auto-generates stubs (one_way_route).
  2. Sets the booking to UPDATED_BY_VENDOR.
  3. Notifies the passenger.

Input (BookingInput)​

FieldTypeRequiredDescription
idStringYesBooking ID.

Example​

Mutation
mutation ConfirmTripCompletion($input: BookingInput!) {
confirmTripCompletion(input: $input) {
id
status
}
}
Variables
{
"input": { "id": "BOOK-001" }
}
Success response
{
"data": {
"confirmTripCompletion": {
"id": "BOOK-001",
"status": "UPDATED_BY_VENDOR"
}
}
}

Errors​

Error messageCause
not_found_booking_requestNo booking request found for this booking under the authenticated vendor.
cannot_update_booking_requestThe booking request is not in COMPLETED status.
not_found_bookingThe booking record could not be found.
cannot_update_bookingThe booking is not in ACCEPTED status.
invalid_drop_off_timeFULL_DAY — drop-off time has not passed yet.
invalid_pick_up_timeONE_WAY_ROUTE — pickup date has not passed yet (evaluated in UTC+7).
missing_timesheetsFULL_DAY — no timesheet data found in booking metadata.
timesheet_N_missing_entries_for_dates: YYYY-MM-DD, ...FULL_DAY — timesheet at index N is missing fully-filled entries for the listed dates.

reviewBooking mutation​

Endpoint: POST /graphql/v1 · Auth: Authorization header — corporate user required.

Allows the corporate user who owns the booking to accept or reject the vendor's trip completion report. The booking must be in UPDATED_BY_VENDOR status and the trip must have already ended.

  • ACCEPT: sets booking → CONFIRMED, updates TripBase to APPROVED, records an approved confirmation entry, notifies passenger.
  • REJECT: requires note; sets booking → back to ACCEPTED (allowing the vendor to correct and resubmit), records a rejected confirmation entry, notifies passenger. The rejected confirmation is accessible via the reviewInfo field on the booking.

Input (ReviewBookingInput)​

FieldTypeRequiredDescription
idStringYesBooking ID.
optionReviewBookingOptionYesACCEPT or REJECT.
noteStringREJECT onlyRejection reason. Required when option is REJECT.

Example​

Mutation
mutation ReviewBooking($input: ReviewBookingInput!) {
reviewBooking(input: $input) {
id
status
reviewInfo { note }
}
}
Variables (accept)
{
"input": { "id": "BOOK-001", "option": "ACCEPT" }
}
Variables (reject)
{
"input": { "id": "BOOK-001", "option": "REJECT", "note": "Odometer readings are incorrect" }
}
Success response (accept)
{
"data": {
"reviewBooking": {
"id": "BOOK-001",
"status": "CONFIRMED",
"reviewInfo": null
}
}
}
Success response (reject)
{
"data": {
"reviewBooking": {
"id": "BOOK-001",
"status": "ACCEPTED",
"reviewInfo": { "note": "Odometer readings are incorrect" }
}
}
}

Errors​

Error messageCause
not_valid_userNo booking request found for this booking belonging to the authenticated user.
not_found_bookingThe booking record could not be found.
cannot_confirm_bookingThe booking is not in UPDATED_BY_VENDOR status.
invalid_drop_off_timeFULL_DAY — drop-off time has not passed yet.
invalid_pick_up_timeONE_WAY_ROUTE — pickup date has not passed yet.
not_have_note_in_reject_confirmationREJECT option was chosen but note was not provided.

booking query​

Endpoint: POST /graphql/v1 · Auth: Authorization header — corporate user or vendor user required.

Fetches a single booking by ID.

ghi chú

Computed status: when the raw DB status is PENDING and a updated_by_user booking history entry exists, the resolver returns UPDATED_BY_USER instead. A PENDING booking with no such history returns null.

onGoing field: computed at query time — true for FULL_DAY bookings where now is between pickUpTime and dropOffTime with ACCEPTED status; true for ONE_WAY_ROUTE bookings where today's date equals the pickup date.

reviewInfo: only populated when status is ACCEPTED — returns the most recent rejected confirmation entry (vendor's resubmission prompt).

route: only populated for ONE_WAY_ROUTE bookings.

Input (BookingInput)​

FieldTypeRequiredDescription
idStringYesBooking ID to fetch.

Example​

Query
query Booking($input: BookingInput!) {
booking(input: $input) {
id
vendorId
corporateId
type
status
price
code
pickUpTime
dropOffTime
onGoing
tripId
contactPerson { fullName email phoneNumber }
passengers { userId fullName email phoneNumber }
pickUpLocation { address location { cityCode countryCode } }
dropOffLocation { address location { cityCode countryCode } }
route { id }
reviewInfo { note }
corporate { companyName displayName logo }
bookingRequest { id status }
files { id }
}
}
Variables
{
"input": { "id": "BOOK-001" }
}
Success response
{
"data": {
"booking": {
"id": "BOOK-001",
"vendorId": "VEND-001",
"corporateId": "CORP-001",
"type": "FULL_DAY",
"status": "ACCEPTED",
"price": "1600000",
"code": "CORP-A1B2C3",
"pickUpTime": "2026-07-01T08:00:00Z",
"dropOffTime": "2026-07-03T08:00:00Z",
"onGoing": false,
"tripId": "TRIP-001",
"route": null,
"reviewInfo": null,
"corporate": { "companyName": "ABC Corp", "displayName": "ABC", "logo": null },
"bookingRequest": { "id": "BR-001", "status": "COMPLETED" },
"files": []
}
}
}

Health​

Auth: None · HTTP Status: 200 OK (healthy) / 503 Service Unavailable (unhealthy)

EndpointPurpose
GET /api/upLiveness check — returns true if the service process is up.

GET /api/up​

Returns true as long as the service process is running. No dependency checks are performed. Logging is disabled for this route.

Auth: None · HTTP Status: 200 OK

Response (200 OK)
true