Car Rental Service
Endpoints
| Endpoint | Domain | Auth |
|---|---|---|
POST /graphql/v1 | Vendor Management | Authorization |
GET /api/up | Health | None |
Authentication
| Scheme | Header | Applies to | On failure |
|---|---|---|---|
| User token | Authorization: <user-token> | POST /graphql/v1 | 401 Unauthorized |
| None | — | /api/up | — |
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)
| Field | Type | Required | Description |
|---|---|---|---|
name | String | Yes | Vendor company name. |
address | String | Yes | Full business address. |
email | String | Yes | Vendor contact email. |
phone_number | String | Yes | Primary contact phone number. |
owner_email | String | Yes | Email of the platform user who will own this vendor account. Resolved to a user_id via the profile service. |
Example
mutation CreateVendor($input: CreateVendorInput!) {
createVendor(input: $input) {
id
name
address
email
phoneNumber
}
}
{
"input": {
"name": "ABC Car Rental",
"address": "123 Nguyen Hue, District 1, HCMC",
"email": "contact@abccar.vn",
"phone_number": "84912345678",
"owner_email": "owner@example.com"
}
}
{
"data": {
"createVendor": {
"id": "VEND-001",
"name": "ABC Car Rental",
"address": "123 Nguyen Hue, District 1, HCMC",
"email": "contact@abccar.vn",
"phoneNumber": "84912345678"
}
}
}
Errors
| Error message | Cause |
|---|---|
not_found_user_id | owner_email does not match any user in the profile service. |
existed_user_id | A vendor already exists for the resolved user_id. |
| Ecto changeset | Input validation failure (e.g. missing required field). |
{
"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)
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | ID of the vendor to update. |
name | String | No | New vendor company name. |
address | String | No | New full business address. |
email | String | No | New vendor contact email. |
phone_number | String | No | New primary contact phone number. |
Example
mutation UpdateVendor($input: UpdateVendorInput!) {
updateVendor(input: $input) {
id
name
address
email
phoneNumber
}
}
{
"input": {
"id": "VEND-001",
"name": "ABC Car Rental Updated",
"address": "456 Le Loi, District 1, HCMC"
}
}
{
"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 message | Cause |
|---|---|
not_valid_vendor | No vendor found for the given id. |
| Ecto changeset | Input 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)
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | ID of the vendor to delete. |
Example
mutation DeleteVendor($input: VendorInput!) {
deleteVendor(input: $input)
}
{
"input": {
"id": "VEND-001"
}
}
{
"data": {
"deleteVendor": true
}
}
Errors
| Error message | Cause |
|---|---|
not_valid_vendor | No 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)
| Field | Type | Required | Description |
|---|---|---|---|
vendor_id | String | Yes | ID of the vendor to assign the staff member to. |
user_id | Integer | Yes | Platform user ID. Verified against the profile service before insert. |
active | Boolean | Yes | Whether the staff member is currently active. |
role | VendorStaffRole | Yes | Role within the vendor. Enum: STAFF, ADMIN. |
Example
mutation CreateVendorStaff($input: CreateVendorStaffInput!) {
createVendorStaff(input: $input) {
id
vendorId
userId
active
role
}
}
{
"input": {
"vendor_id": "VEND-001",
"user_id": 42,
"active": true,
"role": "STAFF"
}
}
{
"data": {
"createVendorStaff": {
"id": "VS-001",
"vendorId": "VEND-001",
"userId": 42,
"active": true,
"role": "STAFF"
}
}
}
Errors
| Error message | Cause |
|---|---|
not_valid_vendor | No vendor found for the given vendor_id. |
not_existed_user | No platform user found for the given user_id. |
existed_vendor_staff | A staff record already exists for this vendor_id + user_id pair. |
| Ecto changeset | Input 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)
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | ID of the vendor staff record to delete. |
Example
mutation DeleteVendorStaff($input: VendorStaffInput!) {
deleteVendorStaff(input: $input)
}
{
"input": {
"id": "VS-001"
}
}
{
"data": {
"deleteVendorStaff": true
}
}
Errors
| Error message | Cause |
|---|---|
not_existed_vendor_staff | No 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)
| Field | Type | Required | Description |
|---|---|---|---|
vendor_id | String | Yes | ID of the vendor to assign the vehicle to. |
vehicle_id | String | Yes | ID of the vehicle to link. |
Example
mutation CreateVendorVehicle($input: CreateVendorVehicleInput!) {
createVendorVehicle(input: $input) {
id
vendorId
vehicleId
}
}
{
"input": {
"vendor_id": "VEND-001",
"vehicle_id": "VEH-007"
}
}
{
"data": {
"createVendorVehicle": {
"id": "VV-001",
"vendorId": "VEND-001",
"vehicleId": "VEH-007"
}
}
}
Errors
| Error message | Cause |
|---|---|
not_found_vendor | No vendor found for the given vendor_id. |
not_found_vehicle | No vehicle found for the given vehicle_id. |
vendor_vehicle_already_exists | This vendor–vehicle pair is already linked. |
| Ecto changeset | Input 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)
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | ID of the vendor vehicle record to delete. |
Example
mutation DeleteVendorVehicle($input: VendorVehicleInput!) {
deleteVendorVehicle(input: $input)
}
{
"input": {
"id": "VV-001"
}
}
{
"data": {
"deleteVendorVehicle": true
}
}
Errors
| Error message | Cause |
|---|---|
not_found_vendor_vehicle | No 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)
| Field | Type | Required | Description |
|---|---|---|---|
name | String | Yes | Region display name. |
status | Boolean | Yes | Whether the region is active. |
time_per_day | Decimal | Yes | Allowed driving hours per day for this region. |
distance_per_day | Decimal | Yes | Allowed driving distance (km) per day for this region. |
provinces | [ProvinceInput!]! | Yes | List of provinces belonging to this region. Must not be empty. |
ProvinceInput
| Field | Type | Required | Description |
|---|---|---|---|
city_code | String | Yes | City/province code. |
country_code | String | Yes | ISO country code (e.g. "VN"). |
name | String | Yes | Province display name. |
Example
mutation CreateRegion($input: CreateRegionInput!) {
createRegion(input: $input) {
id
vendorId
name
status
timePerDay
distancePerDay
provinces {
id
cityCode
countryCode
name
}
}
}
{
"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" }
]
}
}
{
"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 message | Cause |
|---|---|
| Ecto changeset | Input 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)
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | ID of the region to update. |
name | String | No | New region display name. |
status | Boolean | No | New active status. |
time_per_day | Decimal | No | New allowed driving hours per day. |
distance_per_day | Decimal | No | New allowed driving distance per day. |
provinces | [ProvinceInput!] | No | Full replacement list of provinces. When provided, all existing provinces are deleted and reinserted. |
Example
mutation UpdateRegion($input: UpdateRegionInput!) {
updateRegion(input: $input) {
id
name
status
timePerDay
distancePerDay
provinces {
cityCode
countryCode
name
}
}
}
{
"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" }
]
}
}
{
"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 message | Cause |
|---|---|
not_found_region | No region found for the given id under the authenticated vendor. |
| Ecto changeset | Input 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)
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | ID of the region to delete. |
Example
mutation DeleteRegion($input: RegionInput!) {
deleteRegion(input: $input)
}
{
"input": {
"id": "REG-001"
}
}
{
"data": {
"deleteRegion": true
}
}
Errors
| Error message | Cause |
|---|---|
not_found_region | No 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)
| Field | Type | Required | Description |
|---|---|---|---|
after | String | No | Cursor for pagination (opaque string). |
limit | Integer | No | Number of records to return. Default: 10. |
Example
query Regions($input: ListRegionsInput!) {
regions(input: $input) {
entries {
id
name
status
timePerDay
distancePerDay
provinces {
cityCode
countryCode
name
}
}
metadata {
after
totalCount
}
}
}
{
"input": { "limit": 10 }
}
{
"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)
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | ID of the region to fetch. |
Example
query Region($input: RegionInput!) {
region(input: $input) {
id
vendorId
name
status
timePerDay
distancePerDay
provinces {
cityCode
countryCode
name
}
}
}
{
"input": { "id": "REG-001" }
}
{
"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)
| Field | Type | Required | Description |
|---|---|---|---|
id | ID | Yes | ID of the full-day rental price record to update. |
overtime_price | Decimal | No | Price charged per hour of overtime beyond time_per_day. |
over_distance_price | Decimal | No | Price charged per km beyond distance_per_day. |
currency | String | No | Currency code (e.g. "VND"). |
num_days_config | [Decimal] | No | Ordered list of day thresholds that define the pricing tiers (e.g. [1, 3, 7]). |
daily_price_tiers | [DailyPriceTierInput] | No | Pricing tiers. Each tier maps a num_days threshold to a price and currency. |
DailyPriceTierInput
| Field | Type | Required | Description |
|---|---|---|---|
num_days | Decimal | No | Day threshold for this tier. |
price | Decimal | No | Price per day at this tier. |
currency | String | No | Currency code for this tier (e.g. "VND"). |
Example
mutation UpdateFullDayRentalPrice($input: UpdateFullDayRentalPriceInput!) {
updateFullDayRentalPrice(input: $input) {
id
vendorId
regionId
vehicleId
overtimePrice
overDistancePrice
currency
numDaysConfig
dailyPriceTiers {
numDays
price
currency
}
}
}
{
"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" }
]
}
}
{
"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 message | Cause |
|---|---|
not_found_full_day_rental_price | No record found for the given id under the authenticated vendor. |
| Ecto changeset | Input 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)
| Field | Type | Required | Description |
|---|---|---|---|
pickup_location | LocationInput | Yes | Pickup city — city_code and country_code used to resolve the region. |
dropoff_location | LocationInput | Yes | Dropoff city — must share an active region with the pickup location. |
pickup_time | DateTime | Yes | Rental start time (ISO 8601). |
dropoff_time | DateTime | Yes | Rental end time (ISO 8601). Used with pickup_time to compute rental days. |
vehicle_id | String | Yes | ID of the vehicle being rented. |
vendor_id | String | Yes | ID of the vendor providing the vehicle. |
Example
query FullDayRentalAmount($input: FullDayRentalAmountInput!) {
fullDayRentalAmount(input: $input) {
amount
currency
}
}
{
"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"
}
}
{
"data": {
"fullDayRentalAmount": {
"amount": "1500000",
"currency": "VND"
}
}
}
Errors
| Error message | Cause |
|---|---|
not_in_same_region | Pickup and dropoff locations do not share an active region for the given vendor. |
full_day_rental_price_not_found | No price record exists for the resolved region + vehicle combination. |
no_price_tier_configured | The price record has no daily_price_tiers entries. |
price_not_configured_for_tier | The matched tier has a nil price — pricing has not been set up for this tier. |
Booking Requests
rejectBookingRequest mutation
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:
- Sets the booking request status to
REJECTED. - Sets the linked booking status to
CANCELLED. - Calls the TripBase product service to update the external trip product status to
REJECTED. - Sends a rejection notification to the passenger via the notification center.
Returns the updated BookingRequest object.
Input (BookingRequestInput)
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | ID of the booking request to reject. |
Example
mutation RejectBookingRequest($input: BookingRequestInput!) {
rejectBookingRequest(input: $input) {
id
vendorId
corporateId
status
pickUpTime
dropOffTime
note
}
}
{
"input": {
"id": "BR-001"
}
}
{
"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 message | Cause |
|---|---|
not_found_booking_request | No booking request found for the given id under the authenticated vendor. |
cannot_reject_booking_request | The booking request exists but is not in REQUESTED status. |
not_found_booking | The 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:
- Validates the vendor exists.
- For
FULL_DAY: resolves the active region from pickup/dropoff city codes. - Calculates the total price across all requested vehicles and quantities.
- Creates a
Bookingrecord with statusPENDING. - Creates the
BookingRequestrecord with statusREQUESTED. - Registers a trip product with TripBase.
- Sends a notification to the passenger.
- Records a creation history entry.
Input (CreateBookingRequestInput)
| Field | Type | Required | Description |
|---|---|---|---|
vendor_id | String | Yes | ID of the vendor to request from. |
corporate_id | String | Yes | ID of the corporate making the request. |
trip_id | String | Yes | TripBase trip ID. The caller must be the trip owner. |
vehicles | [VehicleQuantityInput!]! | Yes | Vehicles and quantities to request. |
type | BookingType | Yes | FULL_DAY or ONE_WAY_ROUTE. |
passengers | [PassengerInput!]! | Yes | List of passengers. |
pick_up_location | PlaceInput | Yes | Pickup address and coordinates. |
pick_up_time | DateTime | Yes | Pickup time (ISO 8601). |
drop_off_location | PlaceInput | Yes | Drop-off address and coordinates. |
drop_off_time | DateTime | FULL_DAY only | Drop-off time. Required when type is FULL_DAY. |
route_id | String | ONE_WAY_ROUTE only | Route ID. Required when type is ONE_WAY_ROUTE. |
begin_location | LocationInput | Yes | Begin location of the trip itinerary that contains this booking request (city_code, country_code). |
end_location | LocationInput | Yes | End location of the trip itinerary that contains this booking request (city_code, country_code). |
note | String | No | Optional note for the vendor. |
VehicleQuantityInput · PlaceInput · PassengerInput
| Type | Fields |
|---|---|
VehicleQuantityInput | id (String, required), quantity (Integer, required) |
PlaceInput | address (String, required), location (LocationInput, required) |
PassengerInput | user_id (Integer), full_name (String), email (String), phone_number (String), is_contact_person (Boolean) |
Example
mutation CreateBookingRequest($input: CreateBookingRequestInput!) {
createBookingRequest(input: $input) {
id
vendorId
corporateId
tripId
status
type
pickUpTime
dropOffTime
code
note
}
}
{
"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" }
}
}
{
"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 message | Cause |
|---|---|
not_trip_owner | The authenticated user is not the owner of the given trip in TripBase. |
not_valid_vendor_id | No vendor found for the given vendor_id. |
not_found_region | FULL_DAY only — pickup/dropoff locations do not share an active region. |
| Ecto changeset | Input 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:
- Guards status (
REQUESTEDorCOMPLETEDonly) and loads the linked booking. - Validates pickup time is not in the past.
- For
ONE_WAY_ROUTE: validatesroute_idis provided and exists. - For
FULL_DAY: validatesdrop_off_timeis provided and resolves the active region. - Recalculates the total price across vehicles.
- Updates the linked
Bookingand theBookingRequest. - Records history entries and syncs to TripBase.
Input (UpdateBookingRequestInput)
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | ID of the booking request to update. |
vehicles | [VehicleQuantityInput!]! | Yes | Replacement vehicle list with quantities. |
pick_up_location | PlaceInput | Yes | New pickup address and coordinates. |
pick_up_time | DateTime | Yes | New pickup time. Must not be in the past. |
drop_off_location | PlaceInput | Yes | New drop-off address and coordinates. |
drop_off_time | DateTime | FULL_DAY only | New drop-off time. Required for FULL_DAY bookings. |
route_id | String | ONE_WAY_ROUTE only | Route ID. Required for ONE_WAY_ROUTE bookings. |
note | String | No | Updated note. |
Example
mutation UpdateBookingRequest($input: UpdateBookingRequestInput!) {
updateBookingRequest(input: $input) {
id
status
pickUpTime
dropOffTime
note
}
}
{
"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"
}
}
{
"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 message | Cause |
|---|---|
not_found_booking_request | No booking request found for the given id. |
not_allow_update_booking_request | Status is not REQUESTED or COMPLETED. |
not_found_booking | The linked booking record could not be found. |
invalid_pick_up_time | pick_up_time is in the past. |
route_id_required | ONE_WAY_ROUTE booking but route_id was not provided. |
not_found_route | The given route_id does not exist. |
drop_off_time_required | FULL_DAY booking but drop_off_time was not provided. |
pick_up_time_must_be_before_drop_off_time | pick_up_time is not earlier than drop_off_time. |
not_found_region | FULL_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:
- Verifies the booking request belongs to the authenticated user with a cancellable status.
- Sets the booking request to
CANCELLEDand records history. - Sets the linked booking to
CANCELLED. - Updates the trip product status in TripBase to
CANCELED. - Syncs the updated booking request to TripBase.
Input (BookingRequestInput)
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | ID of the booking request to cancel. |
Example
mutation CancelBookingRequest($input: BookingRequestInput!) {
cancelBookingRequest(input: $input) {
id
status
}
}
{
"input": { "id": "BR-001" }
}
{
"data": {
"cancelBookingRequest": {
"id": "BR-001",
"status": "CANCELLED"
}
}
}
Errors
| Error message | Cause |
|---|---|
not_found_booking_request | No booking request found for the given id. |
not_trip_owner | The authenticated user is not the trip owner in TripBase. |
unauthorized | The booking request belongs to a different user. |
not_allow_cancel_booking_request | Status is not REQUESTED or ACCEPTED. |
not_found_booking | The 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)
| Field | Type | Required | Description |
|---|---|---|---|
after | String | No | Cursor for pagination. |
limit | Integer | No | Records to return. Default: 20. |
filter.corporate_id | String | No | Filter by corporate. |
filter.text | String | No | Case-insensitive search across booking code, contact person full_name, email, phone_number. |
Example
query BookingRequests($input: ListBookingRequestsInput!) {
bookingRequests(input: $input) {
entries {
id
status
type
code
pickUpTime
dropOffTime
contactPerson { fullName email phoneNumber }
}
metadata { after totalCount }
}
}
{
"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_locationare all provided: filters by exact trip and route match, scoped toREQUESTEDorCOMPLETEDstatus. No corporate role check. - List mode — when
trip_idis absent: requires corporate roleOWNERorADMIN. Returns all booking requests for the corporate with optional text search. Includes total count.
Input (ListBookingRequestsInput)
| Field | Type | Required | Description |
|---|---|---|---|
after | String | No | Cursor for pagination. |
limit | Integer | No | Records to return. Default: 20. |
filter.trip_id | String | Trip mode | When combined with locations, activates trip mode. |
filter.begin_location | LocationInput | Trip mode | Origin city. Required in trip mode. |
filter.end_location | LocationInput | Trip mode | Destination city. Required in trip mode. |
filter.text | String | No (list mode) | Searches booking code, contact person full_name, email, phone_number. |
Example
query CorporateBookingRequests($input: ListBookingRequestsInput!) {
corporateBookingRequests(input: $input) {
entries {
id
status
type
code
pickUpTime
dropOffTime
}
metadata { after totalCount }
}
}
{
"input": { "limit": 20, "filter": { "text": "BR-" } }
}
{
"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 message | Cause |
|---|---|
unauthorized | List mode — corporate user lacks OWNER or ADMIN role. |
required_corporate | No 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.
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)
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | ID of the booking request to fetch. |
Example
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 }
}
}
{
"input": { "id": "BR-001" }
}
{
"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:
- Sets the booking request to
COMPLETED. - Sets the booking to
ACCEPTEDand storesvehicles_driversin metadata. - Syncs the updated booking to TripBase.
- Notifies the passenger.
Input (CreateBookingInput)
| Field | Type | Required | Description |
|---|---|---|---|
booking_request_id | String | Yes | ID of the booking request to accept. |
vehicles_drivers | [VehicleDriverInput!] | Yes | Driver assignments — one entry per vehicle unit. Count per vehicle_id must match the requested quantity. |
VehicleDriverInput
| Field | Type | Required | Description |
|---|---|---|---|
vehicle_id | String | Yes | Vehicle ID (must be in the booking request). |
driver_name | String | Yes | Driver full name. |
driver_phone_number | String | Yes | Driver contact phone number. |
vehicle_plate_number | String | Yes | Vehicle plate number. |
Example
mutation CreateBooking($input: CreateBookingInput!) {
createBooking(input: $input) {
id
status
price
pickUpTime
dropOffTime
}
}
{
"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"
}
]
}
}
{
"data": {
"createBooking": {
"id": "BOOK-001",
"status": "ACCEPTED",
"price": "1600000",
"pickUpTime": "2026-07-01T08:00:00Z",
"dropOffTime": "2026-07-03T08:00:00Z"
}
}
}
Errors
| Error message | Cause |
|---|---|
not_found_booking_request | No booking request found for the given ID under the authenticated vendor. |
cannot_create_booking | The booking request is not in REQUESTED status. |
not_found_booking | The linked booking record could not be found. |
invalid_pick_up_time | The pickup time is not in the future. |
invalid_vehicles_drivers_format | vehicles_drivers is empty or not provided. |
invalid_vehicle_in_vehicles_drivers | A vehicle_id in vehicles_drivers was not part of the original booking request. |
vehicles_drivers_quantity_exceeds_requested | The 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:
- Replaces
vehicles_driversin the booking metadata. - Syncs the updated booking to TripBase.
- Notifies the passenger.
Input (UpdateBookingInput)
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | Booking ID to update. |
vehicles_drivers | [VehicleDriverInput!] | Yes | New driver assignments. Count per vehicle_id must match the requested quantity. |
Example
mutation UpdateBooking($input: UpdateBookingInput!) {
updateBooking(input: $input) {
id
status
}
}
{
"input": {
"id": "BOOK-001",
"vehicles_drivers": [
{
"vehicle_id": "VEH-007",
"driver_name": "Le Van C",
"driver_phone_number": "84911111111",
"vehicle_plate_number": "51A-99999"
}
]
}
}
{
"data": {
"updateBooking": {
"id": "BOOK-001",
"status": "ACCEPTED"
}
}
}
Errors
| Error message | Cause |
|---|---|
not_found_booking_request | No booking request found for this booking under the authenticated vendor. |
cannot_update_booking_request | The booking request is not in COMPLETED status. |
invalid_pick_up_time | The pickup time is not in the future. |
not_found_booking | The booking record could not be found. |
cannot_update_booking | The booking is not in ACCEPTED status. |
invalid_vehicle_in_vehicles_drivers | A vehicle_id in vehicles_drivers was not part of the original booking request. |
vehicles_drivers_quantity_exceeds_requested | The 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_feesper entry.ONE_WAY_ROUTE:route_base_price + total_toll_feesacross all entries.
Input (UpdateFinishedBookingInput)
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | Booking ID. |
timesheets | [TimesheetInput!] | Yes | One timesheet per vehicle driver. Must be non-empty with at least one entry each. |
TimesheetInput
| Field | Type | Required | Description |
|---|---|---|---|
vehicle_driver | VehicleDriverInput | Yes | Driver and vehicle identification. |
timesheet_entries | [TimesheetEntryInput!] | Yes | Daily trip records. Must be non-empty. |
TimesheetEntryInput
| Field | Type | Required | Description |
|---|---|---|---|
date | DateTime | Yes | Date of the trip entry (used for FULL_DAY date validation). |
toll_fee | Decimal | Yes | Toll fees incurred on this day. |
start_working_time | DateTime | No | Start of driving time. Used for overtime calculation. |
end_working_time | DateTime | No | End of driving time. Used for overtime calculation. |
start_odometer | Decimal | No | Starting odometer reading (km). Used for excess distance calc. |
end_odometer | Decimal | No | Ending odometer reading (km). Used for excess distance calc. |
route | String | No | Route description for this entry. |
Example
mutation UpdateFinishedBooking($input: UpdateFinishedBookingInput!) {
updateFinishedBooking(input: $input) {
id
status
price
}
}
{
"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"
}
]
}
]
}
}
{
"data": {
"updateFinishedBooking": {
"id": "BOOK-001",
"status": "ACCEPTED",
"price": "1700000"
}
}
}
Errors
| Error message | Cause |
|---|---|
not_found_booking_request | No booking request found for this booking under the authenticated vendor. |
cannot_update_booking_request | The booking request is not in COMPLETED status. |
not_found_booking | The booking record could not be found. |
cannot_update_booking | The booking is not in ACCEPTED status. |
invalid_drop_off_time | The pickup time has not passed yet — the trip has not started. |
invalid_timesheets_format | timesheets 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:
- Validates timesheets (full_day) or auto-generates stubs (one_way_route).
- Sets the booking to
UPDATED_BY_VENDOR. - Notifies the passenger.
Input (BookingInput)
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | Booking ID. |
Example
mutation ConfirmTripCompletion($input: BookingInput!) {
confirmTripCompletion(input: $input) {
id
status
}
}
{
"input": { "id": "BOOK-001" }
}
{
"data": {
"confirmTripCompletion": {
"id": "BOOK-001",
"status": "UPDATED_BY_VENDOR"
}
}
}
Errors
| Error message | Cause |
|---|---|
not_found_booking_request | No booking request found for this booking under the authenticated vendor. |
cannot_update_booking_request | The booking request is not in COMPLETED status. |
not_found_booking | The booking record could not be found. |
cannot_update_booking | The booking is not in ACCEPTED status. |
invalid_drop_off_time | FULL_DAY — drop-off time has not passed yet. |
invalid_pick_up_time | ONE_WAY_ROUTE — pickup date has not passed yet (evaluated in UTC+7). |
missing_timesheets | FULL_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 toAPPROVED, records an approved confirmation entry, notifies passenger.REJECT: requiresnote; sets booking → back toACCEPTED(allowing the vendor to correct and resubmit), records a rejected confirmation entry, notifies passenger. The rejected confirmation is accessible via thereviewInfofield on the booking.
Input (ReviewBookingInput)
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | Booking ID. |
option | ReviewBookingOption | Yes | ACCEPT or REJECT. |
note | String | REJECT only | Rejection reason. Required when option is REJECT. |
Example
mutation ReviewBooking($input: ReviewBookingInput!) {
reviewBooking(input: $input) {
id
status
reviewInfo { note }
}
}
{
"input": { "id": "BOOK-001", "option": "ACCEPT" }
}
{
"input": { "id": "BOOK-001", "option": "REJECT", "note": "Odometer readings are incorrect" }
}
{
"data": {
"reviewBooking": {
"id": "BOOK-001",
"status": "CONFIRMED",
"reviewInfo": null
}
}
}
{
"data": {
"reviewBooking": {
"id": "BOOK-001",
"status": "ACCEPTED",
"reviewInfo": { "note": "Odometer readings are incorrect" }
}
}
}
Errors
| Error message | Cause |
|---|---|
not_valid_user | No booking request found for this booking belonging to the authenticated user. |
not_found_booking | The booking record could not be found. |
cannot_confirm_booking | The booking is not in UPDATED_BY_VENDOR status. |
invalid_drop_off_time | FULL_DAY — drop-off time has not passed yet. |
invalid_pick_up_time | ONE_WAY_ROUTE — pickup date has not passed yet. |
not_have_note_in_reject_confirmation | REJECT 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.
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)
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | Booking ID to fetch. |
Example
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 }
}
}
{
"input": { "id": "BOOK-001" }
}
{
"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)
| Endpoint | Purpose |
|---|---|
GET /api/up | Liveness 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
true