@agenzo/merchant-cli 0.4.3 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -10
- package/dist/index.js +2565 -51
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -18,6 +18,9 @@ var CODE_NUM = {
|
|
|
18
18
|
AUTH_INVITE_CODE_INVALID: 1004,
|
|
19
19
|
KEY_INVALID: 1101,
|
|
20
20
|
KEY_SCOPE_DENIED: 1102,
|
|
21
|
+
PAYMENT_METHOD_NOT_FOUND: 1401,
|
|
22
|
+
PAYMENT_METHOD_DISABLED: 1402,
|
|
23
|
+
INVALID_PAYMENT_METHOD: 1403,
|
|
21
24
|
RESOURCE_NOT_FOUND: 2001,
|
|
22
25
|
RESOURCE_CONFLICT: 2002,
|
|
23
26
|
RESOURCE_STATE_INVALID: 2003,
|
|
@@ -38,6 +41,14 @@ var CODE_NUM = {
|
|
|
38
41
|
QUOTE_EXPIRED: 4202,
|
|
39
42
|
BOOKING_FAILED: 4203,
|
|
40
43
|
CANCELLATION_NOT_ALLOWED: 4204,
|
|
44
|
+
NO_AVAILABILITY: 4301,
|
|
45
|
+
PRICE_CHANGED: 4302,
|
|
46
|
+
NAME_FORMAT_INVALID: 4303,
|
|
47
|
+
HOTEL_ORDER_NOT_FOUND: 4304,
|
|
48
|
+
ALREADY_CANCELLED: 4305,
|
|
49
|
+
CHECKOUT_NOT_ALLOWED: 4306,
|
|
50
|
+
CHECKOUT_TASK_NOT_FOUND: 4307,
|
|
51
|
+
PAY_PER_CALL_NOT_AVAILABLE: 4308,
|
|
41
52
|
RATE_LIMITED: 5001,
|
|
42
53
|
UPSTREAM_ERROR: 5101,
|
|
43
54
|
INTERNAL_ERROR: 5201,
|
|
@@ -55,23 +66,29 @@ function codeNum(code) {
|
|
|
55
66
|
return CODE_NUM[code];
|
|
56
67
|
}
|
|
57
68
|
var CliError = class _CliError extends Error {
|
|
58
|
-
constructor(code, message, statusCode, requestId, upstream) {
|
|
69
|
+
constructor(code, message, statusCode, requestId, upstream, backendMessage) {
|
|
59
70
|
super(message);
|
|
60
71
|
this.code = code;
|
|
61
72
|
this.statusCode = statusCode;
|
|
62
73
|
this.requestId = requestId;
|
|
63
74
|
this.upstream = upstream;
|
|
75
|
+
this.backendMessage = backendMessage;
|
|
64
76
|
this.name = "CliError";
|
|
65
77
|
}
|
|
66
78
|
code;
|
|
67
79
|
statusCode;
|
|
68
80
|
requestId;
|
|
69
81
|
upstream;
|
|
82
|
+
backendMessage;
|
|
70
83
|
/**
|
|
71
84
|
* Map a backend ApiError to a CLI-facing code (§8.5 transitional mapping).
|
|
72
|
-
* Backend numeric codes / HTTP status are translated to §8.4 string codes
|
|
73
|
-
*
|
|
74
|
-
*
|
|
85
|
+
* Backend numeric codes / HTTP status are translated to §8.4 string codes.
|
|
86
|
+
* The top-level `message` is always the stable, catalog-owned text for the
|
|
87
|
+
* resolved code (§8.1 principle 4 — never let raw backend text become the
|
|
88
|
+
* primary/stable message). The backend's own message is NOT discarded,
|
|
89
|
+
* though: it is preserved on `backendMessage` and surfaced in the envelope
|
|
90
|
+
* as `backend_message` so the caller can see the real reason (e.g. which
|
|
91
|
+
* field failed validation) alongside the stable code/message.
|
|
75
92
|
*
|
|
76
93
|
* The optional `opts.auth` selects the auth semantics for the 401 mapping:
|
|
77
94
|
* - `'bearer'` (default, admin-cli Bearer Token): 401 → `AUTH_SESSION_EXPIRED`.
|
|
@@ -95,7 +112,8 @@ var CliError = class _CliError extends Error {
|
|
|
95
112
|
STABLE_MESSAGE[error.code] ?? "Request failed. Please check your input and retry.",
|
|
96
113
|
error.statusCode,
|
|
97
114
|
error.requestId,
|
|
98
|
-
error.upstream
|
|
115
|
+
error.upstream,
|
|
116
|
+
error.errorMessage
|
|
99
117
|
);
|
|
100
118
|
}
|
|
101
119
|
const status = error.statusCode;
|
|
@@ -112,7 +130,8 @@ var CliError = class _CliError extends Error {
|
|
|
112
130
|
STABLE_MESSAGE[code] ?? "Request failed. Please check your input and retry.",
|
|
113
131
|
status,
|
|
114
132
|
error.requestId,
|
|
115
|
-
error.upstream
|
|
133
|
+
error.upstream,
|
|
134
|
+
error.errorMessage
|
|
116
135
|
);
|
|
117
136
|
}
|
|
118
137
|
};
|
|
@@ -167,6 +186,9 @@ var STABLE_MESSAGE = {
|
|
|
167
186
|
AUTH_INVITE_CODE_INVALID: "The invitation code is invalid.",
|
|
168
187
|
KEY_INVALID: "The API key is invalid or has been revoked. Please check your --api-key and retry.",
|
|
169
188
|
KEY_SCOPE_DENIED: "This API key does not have the required scope for this command.",
|
|
189
|
+
PAYMENT_METHOD_NOT_FOUND: "Payment method not found.",
|
|
190
|
+
PAYMENT_METHOD_DISABLED: "This payment method has been disabled.",
|
|
191
|
+
INVALID_PAYMENT_METHOD: "The payment method is not available for this operation.",
|
|
170
192
|
RESOURCE_NOT_FOUND: "The resource was not found or does not belong to the current organization.",
|
|
171
193
|
RESOURCE_CONFLICT: "A resource with the same unique value already exists.",
|
|
172
194
|
RESOURCE_STATE_INVALID: "The resource is in a state that does not permit this operation.",
|
|
@@ -189,17 +211,28 @@ var STABLE_MESSAGE = {
|
|
|
189
211
|
QUOTE_EXPIRED: "The quote has expired. Please request a new quote and retry.",
|
|
190
212
|
BOOKING_FAILED: "The booking could not be completed. Please retry.",
|
|
191
213
|
CANCELLATION_NOT_ALLOWED: "This order cannot be cancelled in its current state.",
|
|
214
|
+
NO_AVAILABILITY: "No rooms available for the selected hotel and dates.",
|
|
215
|
+
PRICE_CHANGED: "The price changed since the quote. Re-quote and confirm.",
|
|
216
|
+
NAME_FORMAT_INVALID: "The guest name format is not accepted; use Latin letters.",
|
|
217
|
+
HOTEL_ORDER_NOT_FOUND: "Hotel order not found.",
|
|
218
|
+
ALREADY_CANCELLED: "This hotel order is already cancelled.",
|
|
219
|
+
CHECKOUT_NOT_ALLOWED: "A partial check-out is not allowed in the current state.",
|
|
220
|
+
CHECKOUT_TASK_NOT_FOUND: "Check-out application not found.",
|
|
221
|
+
PAY_PER_CALL_NOT_AVAILABLE: "Pay-per-call billing is not available; use monthly_settlement.",
|
|
192
222
|
NOT_IMPLEMENTED: "This operation is not implemented yet."
|
|
193
223
|
};
|
|
194
224
|
function toErrorEnvelope(error) {
|
|
195
225
|
if (error instanceof CliError) {
|
|
226
|
+
const stableMessage = error.message || "Unknown error";
|
|
227
|
+
const backendMessage = error.backendMessage && error.backendMessage !== stableMessage ? error.backendMessage : void 0;
|
|
196
228
|
return {
|
|
197
229
|
error: {
|
|
198
230
|
code: error.code,
|
|
199
231
|
code_num: codeNum(error.code),
|
|
200
|
-
message:
|
|
232
|
+
message: stableMessage,
|
|
201
233
|
...error.requestId ? { request_id: error.requestId } : {},
|
|
202
|
-
...error.upstream ? { upstream: error.upstream } : {}
|
|
234
|
+
...error.upstream ? { upstream: error.upstream } : {},
|
|
235
|
+
...backendMessage ? { backend_message: backendMessage } : {}
|
|
203
236
|
}
|
|
204
237
|
};
|
|
205
238
|
}
|
|
@@ -262,13 +295,27 @@ function isBelow(current, minimum) {
|
|
|
262
295
|
var ApiClient = class {
|
|
263
296
|
baseUrl;
|
|
264
297
|
timeout;
|
|
298
|
+
/**
|
|
299
|
+
* Correlation id for this CLI invocation. Generated once per `ApiClient`
|
|
300
|
+
* instance (i.e. once per command execution — each `apps/*` entrypoint
|
|
301
|
+
* constructs exactly one `ApiClient` per process run) and sent as
|
|
302
|
+
* `X-Request-Id` on every request this client makes. The backend's
|
|
303
|
+
* `RequestIdMiddleware` adopts a client-supplied `X-Request-Id` verbatim as
|
|
304
|
+
* both `trace_id` and `request_id` (only generating its own when the header
|
|
305
|
+
* is absent), so every HTTP call belonging to one CLI operation — including
|
|
306
|
+
* multi-step flows like `payment-methods add`'s create + poll + get — shares
|
|
307
|
+
* a single trace_id server-side and can be followed as one chain in logs.
|
|
308
|
+
*/
|
|
309
|
+
requestId;
|
|
265
310
|
constructor(config) {
|
|
266
311
|
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
267
312
|
this.timeout = config.timeout ?? 6e4;
|
|
313
|
+
this.requestId = crypto.randomUUID();
|
|
268
314
|
}
|
|
269
315
|
buildHeaders(auth) {
|
|
270
316
|
const headers = {
|
|
271
|
-
"User-Agent": `agenzo-admin-cli/${getCurrentVersion()}
|
|
317
|
+
"User-Agent": `agenzo-admin-cli/${getCurrentVersion()}`,
|
|
318
|
+
"X-Request-Id": this.requestId
|
|
272
319
|
};
|
|
273
320
|
if (auth.type === "bearer") {
|
|
274
321
|
headers["Authorization"] = `Bearer ${auth.token}`;
|
|
@@ -806,6 +853,7 @@ async function watchRideStatus(apiClient, apiKey, orderId, opts) {
|
|
|
806
853
|
// src/verb-schema.ts
|
|
807
854
|
var CLI_NAME = "agenzo-merchant-cli";
|
|
808
855
|
var NOUN = "ride-elife";
|
|
856
|
+
var HOTEL_NOUN = "hotel-redaug";
|
|
809
857
|
function wantsJsonSchema(argv = process.argv) {
|
|
810
858
|
for (let i = 0; i < argv.length; i++) {
|
|
811
859
|
const a = argv[i];
|
|
@@ -1071,6 +1119,623 @@ var listOrdersSchema = {
|
|
|
1071
1119
|
PARAM_INVALID: "Ensure --page / --page-size are positive integers, then retry."
|
|
1072
1120
|
}
|
|
1073
1121
|
};
|
|
1122
|
+
var hotelSearchSchema = {
|
|
1123
|
+
cli: CLI_NAME,
|
|
1124
|
+
noun: HOTEL_NOUN,
|
|
1125
|
+
verb: "search",
|
|
1126
|
+
description: "Search hotels near a coordinate OR within a destination for the given stay dates. Two location branches: --destination-id (from find-destination) XOR --lat/--lng (geocoded by the Agent backend). Exactly one branch must be supplied",
|
|
1127
|
+
flags: {
|
|
1128
|
+
"destination-id": { type: "string", required: "conditional", description: "Destination ID from find-destination (alternative to --lat/--lng)", constraints: "Exactly one location branch: --destination-id XOR (--lat + --lng)" },
|
|
1129
|
+
lat: { type: "float", required: "conditional", description: "Latitude of the search center (geocoded by the Agent backend, not by the CLI)", constraints: "-90 to 90; requires --lng; mutually exclusive with --destination-id" },
|
|
1130
|
+
lng: { type: "float", required: "conditional", description: "Longitude of the search center (geocoded by the Agent backend, not by the CLI)", constraints: "-180 to 180; requires --lat; mutually exclusive with --destination-id" },
|
|
1131
|
+
distance: { type: "int", required: false, default: 20, description: "Search radius in kilometers", constraints: 'Agent infers from phrasing \u2014 small (2-5) for "right next to a spot", larger (15-30) for a whole city. Do NOT ask the user for a number' },
|
|
1132
|
+
"check-in": { type: "string", required: true, description: "Check-in date", constraints: "YYYY-MM-DD, today or later" },
|
|
1133
|
+
"check-out": { type: "string", required: true, description: "Check-out date", constraints: "YYYY-MM-DD, strictly after check-in" },
|
|
1134
|
+
adults: { type: "int", required: false, default: 2, description: "Number of adults", constraints: "1-9 per room" },
|
|
1135
|
+
children: { type: "int", required: false, default: 0, description: "Number of children" },
|
|
1136
|
+
"room-num": { type: "int", required: false, default: 1, description: "Number of rooms requested" },
|
|
1137
|
+
star: { type: "int", required: false, description: "Minimum hotel star rating filter", constraints: "Enum 3 / 4 / 5" },
|
|
1138
|
+
keyword: { type: "string", required: false, description: "Optional hotel name keyword to narrow results" },
|
|
1139
|
+
"price-min": { type: "float", required: false, description: "Minimum price filter" },
|
|
1140
|
+
"price-max": { type: "float", required: false, description: "Maximum price filter" },
|
|
1141
|
+
"sort-by": { type: "string", required: false, description: "Sort field" },
|
|
1142
|
+
page: { type: "int", required: false, default: 1, description: "Page number", constraints: ">= 1" },
|
|
1143
|
+
"page-size": { type: "int", required: false, default: 10, description: "Results per page", constraints: ">= 1" },
|
|
1144
|
+
"hotel-facility-codes": { type: "string", required: false, description: "Hotel facility codes (comma-separated or JSON array, from hotel-filters)" },
|
|
1145
|
+
"room-facility-codes": { type: "string", required: false, description: "Room facility codes (comma-separated or JSON array, from hotel-filters)" },
|
|
1146
|
+
"hotel-brand-codes": { type: "string", required: false, description: "Hotel brand codes (comma-separated or JSON array, from hotel-filters)" },
|
|
1147
|
+
"plate-codes": { type: "string", required: false, description: "Plate codes (comma-separated or JSON array, from hotel-filters)" },
|
|
1148
|
+
"hotel-label-ids": { type: "string", required: false, description: "Hotel label IDs (comma-separated or JSON array, from hotel-filters)" },
|
|
1149
|
+
"hotel-sub-category-ids": { type: "string", required: false, description: "Hotel sub-category IDs (comma-separated or JSON array, from hotel-filters)" }
|
|
1150
|
+
},
|
|
1151
|
+
response: {
|
|
1152
|
+
hotels: {
|
|
1153
|
+
type: "array",
|
|
1154
|
+
description: "Matching hotels for the location and dates. Empty when nothing was found (rendered as a successful empty result, not an error).",
|
|
1155
|
+
items: {
|
|
1156
|
+
hotel_id: { type: "string|int", description: "Opaque hotel identifier. Pass verbatim to quote --hotel-id. Do NOT interpret." },
|
|
1157
|
+
hotel_name: { type: "string", description: "Hotel name." },
|
|
1158
|
+
star: { type: "int|null", description: "Star rating." },
|
|
1159
|
+
address: { type: "string|null", description: "Hotel address." },
|
|
1160
|
+
distance_km: { type: "float|null", description: "Approximate distance from the search center, in km." },
|
|
1161
|
+
city_name: { type: "string|null", description: "City name." },
|
|
1162
|
+
district_name: { type: "string|null", description: "District name." },
|
|
1163
|
+
business_name: { type: "string|null", description: "Business area name." },
|
|
1164
|
+
score: { type: "float|null", description: "Hotel score/rating." },
|
|
1165
|
+
main_image: { type: "string|null", description: "Main hotel image URL." },
|
|
1166
|
+
lowest_price: {
|
|
1167
|
+
type: "object|null",
|
|
1168
|
+
description: "Indicative nightly starting price (not the bookable total \u2014 get the real price from quote).",
|
|
1169
|
+
properties: {
|
|
1170
|
+
amount: { type: "float", description: "Starting price in DECIMAL units (NOT cents)." },
|
|
1171
|
+
currency: { type: "string", description: "ISO 4217 currency code." }
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
},
|
|
1177
|
+
example: {
|
|
1178
|
+
command: "agenzo-merchant-cli hotel-redaug search --destination-id D12345 --check-in 2026-07-04 --check-out 2026-07-05 --adults 2",
|
|
1179
|
+
output_summary: "Returns hotels[] for the location. Pick one and use hotels[].hotel_id in hotel-redaug quote. Alternatively use --lat/--lng instead of --destination-id."
|
|
1180
|
+
},
|
|
1181
|
+
error_recovery: {
|
|
1182
|
+
NO_AVAILABILITY: "No hotels near this coordinate for these dates. Widen --distance, try different dates, or have the Agent backend geocode a nearby area. As a last resort try another category=hotel service (see cross_service_recovery).",
|
|
1183
|
+
UPSTREAM_ERROR: "Transient upstream error. Retry once after ~2s backoff. Otherwise surface to user."
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
var hotelQuoteSchema = {
|
|
1187
|
+
cli: CLI_NAME,
|
|
1188
|
+
noun: HOTEL_NOUN,
|
|
1189
|
+
verb: "quote",
|
|
1190
|
+
description: "Get real-time room types and bookable rates for one hotel and stay. Returns rates each carrying an opaque product_token and per-night price_items that must be passed verbatim to create-order. Run immediately before create-order \u2014 availability and prices change in real time",
|
|
1191
|
+
flags: {
|
|
1192
|
+
"hotel-id": { type: "string", required: true, description: "Hotel chosen from search results (search.response.hotels[].hotel_id)" },
|
|
1193
|
+
"check-in": { type: "string", required: true, description: "Check-in date (match search)", constraints: "YYYY-MM-DD" },
|
|
1194
|
+
"check-out": { type: "string", required: true, description: "Check-out date (match search)", constraints: "YYYY-MM-DD, strictly after check-in" },
|
|
1195
|
+
adults: { type: "int", required: false, default: 2, description: "Adults per room (match search)" },
|
|
1196
|
+
children: { type: "int", required: false, default: 0, description: "Children per room (match search)" },
|
|
1197
|
+
"room-num": { type: "int", required: false, default: 1, description: "Number of rooms (match search)" },
|
|
1198
|
+
nationality: { type: "string", required: false, default: "CN", description: "Guest nationality (ISO country code). Affects rate eligibility for some hotels" }
|
|
1199
|
+
},
|
|
1200
|
+
response: {
|
|
1201
|
+
rates: {
|
|
1202
|
+
type: "array",
|
|
1203
|
+
description: "Bookable room+rate options. Empty when the hotel has no rooms for these dates (treat as NO_AVAILABILITY: re-quote another hotel or change dates).",
|
|
1204
|
+
items: {
|
|
1205
|
+
product_token: { type: "string", description: "Opaque token packing room/rate/supply identifiers. Pass verbatim to create-order --product-token. Do NOT interpret or reuse across quotes." },
|
|
1206
|
+
room_name: { type: "string", description: "Room type name." },
|
|
1207
|
+
rate_plan_name: { type: "string|null", description: "Rate plan name (e.g. 'Breakfast included, free cancellation')." },
|
|
1208
|
+
breakfast: { type: "int|null", description: "Breakfast enum for the rate (-1=bed breakfast, 0=no breakfast, 1+=N breakfasts). Display only." },
|
|
1209
|
+
free_cancellation: { type: "bool|null", description: "Whether this rate is currently in a free-cancellation window. Prefer when the user wants flexibility." },
|
|
1210
|
+
total_price: {
|
|
1211
|
+
type: "object",
|
|
1212
|
+
description: "Total bookable price for the whole stay and room count.",
|
|
1213
|
+
properties: {
|
|
1214
|
+
amount: { type: "float", description: "Total price in DECIMAL units. Pass verbatim to create-order --total-amount." },
|
|
1215
|
+
currency: { type: "string", description: "ISO 4217 currency code. Pass to create-order --currency." }
|
|
1216
|
+
}
|
|
1217
|
+
},
|
|
1218
|
+
price_items: {
|
|
1219
|
+
type: "array",
|
|
1220
|
+
description: "Per-night price breakdown. Pass this array verbatim to create-order --price-items.",
|
|
1221
|
+
items: {
|
|
1222
|
+
sale_date: { type: "string", description: "Night date YYYY-MM-DD." },
|
|
1223
|
+
sale_price: { type: "float", description: "Price for that night in DECIMAL units." },
|
|
1224
|
+
breakfast_num: { type: "int", description: "Breakfast enum for that night (-1=bed breakfast, 0=no breakfast, 1+=N). Copy verbatim into create-order; never alter." }
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
},
|
|
1230
|
+
example: {
|
|
1231
|
+
command: "agenzo-merchant-cli hotel-redaug quote --hotel-id 10583772 --check-in 2026-08-20 --check-out 2026-08-21 --adults 2",
|
|
1232
|
+
output_summary: "Returns rates[]. Use rates[].product_token, total_price.amount/currency and price_items[] in hotel-redaug create-order."
|
|
1233
|
+
},
|
|
1234
|
+
error_recovery: {
|
|
1235
|
+
NO_AVAILABILITY: "This hotel has no bookable rooms for these dates (upstream roomItems empty). Re-quote a different hotel from the search result, or re-search with other dates. Do NOT retry the same hotel/dates.",
|
|
1236
|
+
PRICE_CHANGED: "Rates shifted since search. Use the prices returned here; do not reuse search lowest_price. No action other than informing the user if materially higher.",
|
|
1237
|
+
UPSTREAM_ERROR: "Transient upstream error. Retry once after ~2s backoff."
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
var hotelCreateOrderSchema = {
|
|
1241
|
+
cli: CLI_NAME,
|
|
1242
|
+
noun: HOTEL_NOUN,
|
|
1243
|
+
verb: "create-order",
|
|
1244
|
+
description: "Create AND pay for a hotel order in one call, using a product_token and price_items from a recent quote. The backend re-checks availability, authorizes/captures payment via the platform payment gateway (funds path decided server-side by the developer billing_mode \u2014 monthly_settlement or pay_per_call), locks the room with the supplier, and returns the order already PAID. There is no separate settlement step. If any step after funds are taken fails, the funds are automatically released/refunded before the error is returned. check-in/check-out, guest counts, total-amount and price-items must match the quote",
|
|
1245
|
+
flags: {
|
|
1246
|
+
"product-token": { type: "string", required: true, description: "Chosen rate token from quote (quote.response.rates[].product_token). Pass verbatim" },
|
|
1247
|
+
"total-amount": { type: "float", required: true, description: "Total price for the stay (quote.response.rates[].total_price.amount)", constraints: "DECIMAL units (NOT cents), 0.01\u2013999999999.99. Must equal the chosen rate total_price.amount" },
|
|
1248
|
+
currency: { type: "string", required: true, description: "ISO 4217 currency code from the chosen rate (quote.response.rates[].total_price.currency)", constraints: "Exactly 3 uppercase letters" },
|
|
1249
|
+
"price-items": { type: "string", required: true, description: "Per-night price breakdown from quote, copied verbatim (including breakfast_num)", constraints: "JSON array of {sale_date, sale_price, breakfast_num}; copy breakfast_num verbatim from quote (-1/0/1+ are distinct breakfast products, never alter); else PARAM_INVALID" },
|
|
1250
|
+
"check-in": { type: "string", required: true, description: "Check-in date (match quote)", constraints: "YYYY-MM-DD" },
|
|
1251
|
+
"check-out": { type: "string", required: true, description: "Check-out date (match quote)", constraints: "YYYY-MM-DD, strictly after check-in" },
|
|
1252
|
+
"room-num": { type: "int", required: false, default: 1, description: "Number of rooms (match quote)" },
|
|
1253
|
+
adults: { type: "int", required: false, default: 2, description: "Adults per room (match quote)", constraints: "1-20" },
|
|
1254
|
+
children: { type: "int", required: false, default: 0, description: "Children per room (match quote)", constraints: "0-10" },
|
|
1255
|
+
nationality: { type: "string", required: false, default: "CN", description: "Guest nationality (match quote)" },
|
|
1256
|
+
"guest-name": { type: "string", required: true, description: "Primary guest name", constraints: "Use the name as the user gives it. If a supplier returns NAME_FORMAT_INVALID, re-collect in Latin letters and retry" },
|
|
1257
|
+
"contact-name": { type: "string", required: true, description: "Booking contact name (may equal guest-name)" },
|
|
1258
|
+
"contact-phone": { type: "string", required: true, description: "Booking contact phone number", constraints: "Digits; collect country code separately in --contact-country-code" },
|
|
1259
|
+
"contact-country-code": { type: "string", required: false, default: "86", description: "Phone country calling code without '+'" },
|
|
1260
|
+
"contact-email": { type: "string", required: false, description: "Booking contact email" },
|
|
1261
|
+
"arrive-time": { type: "string", required: false, description: "Expected arrival time", constraints: "HH:mm, hotel local time" },
|
|
1262
|
+
"special-requests": { type: "string", required: false, description: "Free-text special requests (non-binding)" },
|
|
1263
|
+
"payment-method-id": { type: "string", required: false, description: "pay_per_call only: charge this SPECIFIC already-bound card instead of the platform auto-selected default/most-recent one. Ignored for monthly_settlement.", constraints: "Must be an ACTIVE card belonging to this developer, else PAYMENT_METHOD_REQUIRED" },
|
|
1264
|
+
"idempotency-key": {
|
|
1265
|
+
type: "string",
|
|
1266
|
+
required: true,
|
|
1267
|
+
description: "Unique key forwarded verbatim as the Idempotency-Key header; never auto-generated. Derive from business intent (e.g. hash(user_id + product_token + check_in)) and reuse the SAME key when retrying the identical request. This single call covers both order creation AND payment",
|
|
1268
|
+
constraints: "1-128 chars [A-Za-z0-9_-]"
|
|
1269
|
+
}
|
|
1270
|
+
},
|
|
1271
|
+
response: {
|
|
1272
|
+
order_id: { type: "string", description: "Our order reference (coOrderCode). Use as --order-id in get / cancel." },
|
|
1273
|
+
fc_order_code: { type: "string", description: "Supplier order reference. Needed as --fc-order-code in cancel and checkout." },
|
|
1274
|
+
total_amount: { type: "float", description: "Amount actually charged (DECIMAL units) \u2014 the order is already PAID by the time this response is returned." },
|
|
1275
|
+
currency: { type: "string", description: "ISO 4217 currency code of the charge." }
|
|
1276
|
+
},
|
|
1277
|
+
example: {
|
|
1278
|
+
command: `agenzo-merchant-cli hotel-redaug create-order --product-token <tok> --total-amount 10.00 --currency CNY --price-items '[{"sale_date":"2026-08-20","sale_price":10.00,"breakfast_num":0}]' --check-in 2026-08-20 --check-out 2026-08-21 --adults 2 --guest-name "Zhang San" --contact-name "Zhang San" --contact-phone 13800138000 --idempotency-key create-h1n2`,
|
|
1279
|
+
output_summary: "Returns {order_id, fc_order_code, total_amount, currency}. The order is already PAID \u2014 proceed to get to poll for supplier CONFIRMED status."
|
|
1280
|
+
},
|
|
1281
|
+
error_recovery: {
|
|
1282
|
+
NO_AVAILABILITY: "Availability vanished between quote and create-order. Re-quote the same hotel; if still empty, quote another hotel or change dates. Use a NEW idempotency-key for the new product_token.",
|
|
1283
|
+
PRICE_CHANGED: "The price/price_items no longer match upstream. Re-quote to get fresh total_price and price_items, confirm the new price with the user, then create-order with the new values and a NEW idempotency-key.",
|
|
1284
|
+
NAME_FORMAT_INVALID: "The supplier requires the guest name in a specific format (commonly Latin first/last name for international properties). Re-collect guest-name in Latin letters and retry with a NEW idempotency-key (the request is no longer identical).",
|
|
1285
|
+
PAYMENT_METHOD_REQUIRED: "pay_per_call developer has no ACTIVE bound card at all, OR the explicitly-passed --payment-method-id is missing/not ACTIVE/does not belong to this developer. Direct the user to bind a card, drop --payment-method-id to auto-select, or pass a valid one.",
|
|
1286
|
+
ACCOUNT_INSUFFICIENT_BALANCE: "monthly_settlement developer settlement credit is insufficient (check via admin-cli accounts get). Direct the user to top up offline; do NOT retry until funded.",
|
|
1287
|
+
ACCOUNT_NOT_FOUND: "monthly_settlement developer has no settlement account. Direct the user to complete contract signing.",
|
|
1288
|
+
ACCOUNT_SUSPENDED: "The settlement account is suspended. Direct the user to contact support; do NOT retry.",
|
|
1289
|
+
PARAM_IDEMPOTENCY_KEY_REQUIRED: "Add a unique --idempotency-key derived from business intent and retry.",
|
|
1290
|
+
PARAM_IDEMPOTENCY_KEY_CONFLICT: "The same idempotency-key was used with different parameters. Retry with the original parameters, or generate a NEW key for the new parameters.",
|
|
1291
|
+
UPSTREAM_ERROR: "Transient upstream error during checkBooking or createOrder. Any funds already taken are automatically released/refunded before this error is returned. Retry once after ~2s backoff with the SAME idempotency-key."
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
var hotelPayOrderSchema = {
|
|
1295
|
+
cli: CLI_NAME,
|
|
1296
|
+
noun: HOTEL_NOUN,
|
|
1297
|
+
verb: "pay-order",
|
|
1298
|
+
description: "Trigger supplier confirmation (upstream payOrder) for an order that was already paid via create-order. Takes only --order-id; no payment parameters needed. On success the supplier begins asynchronous confirmation \u2014 poll get until CONFIRMED (3). Idempotent: calling pay-order on an already-confirmed order replays the existing result",
|
|
1299
|
+
flags: {
|
|
1300
|
+
"order-id": { type: "string", required: true, description: "Order to confirm with the supplier (create-order.response.order_id)." },
|
|
1301
|
+
"idempotency-key": {
|
|
1302
|
+
type: "string",
|
|
1303
|
+
required: true,
|
|
1304
|
+
description: "Unique key forwarded verbatim as the Idempotency-Key header; never auto-generated. Derive from business intent and reuse the SAME key when retrying the same pay-order call",
|
|
1305
|
+
constraints: "1-128 chars [A-Za-z0-9_-]"
|
|
1306
|
+
},
|
|
1307
|
+
watch: { type: "bool", required: false, default: false, description: "Retained for backward compatibility. Each iteration emits one NDJSON line" },
|
|
1308
|
+
"watch-interval": { type: "int", required: false, default: 5, description: "Seconds between poll attempts in --watch mode" },
|
|
1309
|
+
"watch-timeout": { type: "int", required: false, default: 300, description: "Total seconds before giving up in --watch mode" }
|
|
1310
|
+
},
|
|
1311
|
+
response: {
|
|
1312
|
+
order_id: { type: "string", description: "Order reference." },
|
|
1313
|
+
settlement_path: { type: "string", description: "The order's billing_mode echoed: 'monthly_settlement' or 'pay_per_call'." },
|
|
1314
|
+
status: { type: "string", description: "'PAID' \u2014 supplier confirmation is asynchronous; poll get until CONFIRMED." },
|
|
1315
|
+
amount: { type: "float", description: "Settled amount in DECIMAL units (from create-order)." },
|
|
1316
|
+
currency: { type: "string", description: "ISO 4217 currency code." }
|
|
1317
|
+
},
|
|
1318
|
+
example: {
|
|
1319
|
+
command: "agenzo-merchant-cli hotel-redaug pay-order --order-id hho_01KWC63Z5CD6CKBM33Q7SC2ZDT --idempotency-key pay-h1n2",
|
|
1320
|
+
output_summary: 'Triggers supplier payOrder (confirmation/ticket issuance). Returns {order_id, settlement_path, status:"PAID", amount, currency}. Then poll get until CONFIRMED.'
|
|
1321
|
+
},
|
|
1322
|
+
error_recovery: {
|
|
1323
|
+
INVALID_ORDER_STATE: "The order is not in a payable state (it may already be confirmed, cancelled, or missing auth). Check status via get; do NOT retry pay-order.",
|
|
1324
|
+
ORDER_NOT_FOUND: "No order for this --order-id. Verify it is the order_id returned by create-order. Do NOT retry.",
|
|
1325
|
+
UPSTREAM_ERROR: "Transient upstream error during supplier payOrder. Retry once after ~5s with the SAME idempotency-key, then poll get to check whether the confirmation actually took effect.",
|
|
1326
|
+
PARAM_IDEMPOTENCY_KEY_REQUIRED: "Add a unique --idempotency-key and retry.",
|
|
1327
|
+
PARAM_IDEMPOTENCY_KEY_CONFLICT: "Same idempotency-key used with different parameters. Use the original parameters, or generate a NEW key."
|
|
1328
|
+
}
|
|
1329
|
+
};
|
|
1330
|
+
var hotelGetSchema = {
|
|
1331
|
+
cli: CLI_NAME,
|
|
1332
|
+
noun: HOTEL_NOUN,
|
|
1333
|
+
verb: "get",
|
|
1334
|
+
description: "Query a hotel order status by id; with --watch, poll until a terminal status. Confirmation from the property is asynchronous, so order_status is usually PROCESSING (2) immediately after book \u2014 poll until CONFIRMED (3)",
|
|
1335
|
+
flags: {
|
|
1336
|
+
"order-id": { type: "string", required: true, description: "Our order reference (coOrderCode) returned by book (book.response.order_id)" },
|
|
1337
|
+
watch: { type: "bool", required: false, default: false, description: "Poll until a terminal status, emitting one NDJSON line per update" },
|
|
1338
|
+
"watch-interval": { type: "int", required: false, default: 5, description: "Seconds between polls when --watch is set" },
|
|
1339
|
+
"watch-timeout": { type: "int", required: false, default: 600, description: "Max seconds to poll before giving up" }
|
|
1340
|
+
},
|
|
1341
|
+
response: {
|
|
1342
|
+
order_id: { type: "string", description: "Our order reference." },
|
|
1343
|
+
fc_order_code: { type: "string", description: "Supplier order reference." },
|
|
1344
|
+
order_status: { type: "string", description: "Std STRING: PROCESSING | CONFIRMED | CANCELLED | COMPLETED | INIT. There is no order_status_std field." },
|
|
1345
|
+
order_status_code: { type: "int", description: "2 = Processing, 3 = Confirmed, 4 = Cancelled, 5 = Completed. Provider path only; absent on the local-cache fallback." },
|
|
1346
|
+
channel_state: { type: "string|null", description: "Channel-level state (e.g. 'paid' = settled)." },
|
|
1347
|
+
hotel_confirm_no: { type: "string|null", description: "Property confirmation number. Populated once order_status reaches CONFIRMED (3); null while PROCESSING (2)." },
|
|
1348
|
+
hotel_name: { type: "string|null", description: "Hotel name." },
|
|
1349
|
+
room_name: { type: "string|null", description: "Booked room type." },
|
|
1350
|
+
check_in: { type: "string|null", description: "Check-in date YYYY-MM-DD." },
|
|
1351
|
+
check_out: { type: "string|null", description: "Check-out date YYYY-MM-DD." },
|
|
1352
|
+
total_amount: { type: "float|null", description: "Total amount in DECIMAL units." },
|
|
1353
|
+
source: { type: "string", description: "'provider' (live upstream, carries order_status_code) or 'local_cache' (fallback, string status only)." }
|
|
1354
|
+
},
|
|
1355
|
+
example: {
|
|
1356
|
+
command: "agenzo-merchant-cli hotel-redaug get --order-id E2E1750000000",
|
|
1357
|
+
output_summary: "Returns order_status (string) + order_status_code (int). Poll every 5s until CONFIRMED (3); hotel_confirm_no appears once confirmed."
|
|
1358
|
+
},
|
|
1359
|
+
polling: {
|
|
1360
|
+
recommended_interval_seconds: 5,
|
|
1361
|
+
terminal_statuses: [3, 4, 5],
|
|
1362
|
+
in_progress_statuses: [2],
|
|
1363
|
+
field_availability: {
|
|
1364
|
+
hotel_confirm_no: "Populated only after order_status reaches CONFIRMED (3). Null while PROCESSING (2)."
|
|
1365
|
+
}
|
|
1366
|
+
},
|
|
1367
|
+
error_recovery: {
|
|
1368
|
+
HOTEL_ORDER_NOT_FOUND: "No order for this --order-id. Verify it is the order_id returned by book. Do NOT retry.",
|
|
1369
|
+
UPSTREAM_ERROR: "Transient upstream error. Retry once after 5s; if still failing, surface to user."
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
var hotelCancelSchema = {
|
|
1373
|
+
cli: CLI_NAME,
|
|
1374
|
+
noun: HOTEL_NOUN,
|
|
1375
|
+
verb: "cancel",
|
|
1376
|
+
description: "Cancel an entire hotel order within the cancellation policy (a cancellation fee may apply). Returns synchronously, but a successful call is acceptance only, NOT proof: poll get until order_status=CANCELLED (4). For partial-night or out-of-policy cancellation, use checkout instead",
|
|
1377
|
+
flags: {
|
|
1378
|
+
"order-id": { type: "string", required: true, description: "Our order reference (coOrderCode) to cancel (book.response.order_id)" },
|
|
1379
|
+
"fc-order-code": { type: "string", required: true, description: "Supplier order reference (book.response.fc_order_code)" },
|
|
1380
|
+
reason: { type: "string", required: false, description: "Cancellation reason" },
|
|
1381
|
+
"idempotency-key": {
|
|
1382
|
+
type: "string",
|
|
1383
|
+
required: true,
|
|
1384
|
+
description: "Unique key forwarded verbatim as the Idempotency-Key header; never auto-generated. Reuse the SAME key when retrying the same cancellation",
|
|
1385
|
+
constraints: "1-128 chars [A-Za-z0-9_-]"
|
|
1386
|
+
}
|
|
1387
|
+
},
|
|
1388
|
+
response: {
|
|
1389
|
+
order_id: { type: "string", description: "Cancelled order reference." },
|
|
1390
|
+
order_status: { type: "string", description: "Status after the cancel request. Acceptance is NOT proof \u2014 poll get until 'CANCELLED' (order_status_code 4)." },
|
|
1391
|
+
cancellation: {
|
|
1392
|
+
type: "object|null",
|
|
1393
|
+
description: "Confirmed-cancellation details (confirmed shape).",
|
|
1394
|
+
properties: {
|
|
1395
|
+
cancellation_fee: { type: "float", description: "Fee charged for cancellation, in DECIMAL units." },
|
|
1396
|
+
reversal_amount: { type: "float", description: "Amount reversed/refunded, in DECIMAL units." },
|
|
1397
|
+
currency: { type: "string", description: "ISO 4217 currency code." }
|
|
1398
|
+
}
|
|
1399
|
+
},
|
|
1400
|
+
cancellation_fee: { type: "float|null", description: "Fee charged for cancellation (confirmed shape), in DECIMAL units." },
|
|
1401
|
+
refund_amount: { type: "float", description: "Amount credited back to the settlement balance (paid - cancellation_fee), in DECIMAL units." },
|
|
1402
|
+
cancel_status: { type: "string|absent", description: "'cancel_pending' when accepted upstream but not yet observed as CANCELLED (accepted-but-pending shape)." },
|
|
1403
|
+
cancel_result: { type: "unknown|absent", description: "Upstream cancel acknowledgement (pending shape). NOT proof of cancellation \u2014 confirm via get." }
|
|
1404
|
+
},
|
|
1405
|
+
example: {
|
|
1406
|
+
command: "agenzo-merchant-cli hotel-redaug cancel --order-id E2E1750000000 --fc-order-code FC1750000000 --idempotency-key cancel-h1n2",
|
|
1407
|
+
output_summary: "Returns the cancellation acknowledgement and any fee/refund. Then poll get until order_status=CANCELLED (4) to confirm."
|
|
1408
|
+
},
|
|
1409
|
+
error_recovery: {
|
|
1410
|
+
CANCELLATION_NOT_ALLOWED: "The order is outside its free/allowed cancellation window or already terminal. Do NOT retry cancel. If the user still wants out (e.g. drop some nights or request a waiver), use checkout instead.",
|
|
1411
|
+
ALREADY_CANCELLED: "Order is already cancelled (order_status=4). Treat as success; confirm via get.",
|
|
1412
|
+
HOTEL_ORDER_NOT_FOUND: "Verify --order-id / --fc-order-code from the book response. Do NOT retry.",
|
|
1413
|
+
UPSTREAM_ERROR: "Transient upstream error. Retry once after 5s with the SAME idempotency-key, then poll get to check whether the cancellation actually took effect.",
|
|
1414
|
+
PARAM_IDEMPOTENCY_KEY_REQUIRED: "Add a unique --idempotency-key derived from business intent and retry.",
|
|
1415
|
+
PARAM_IDEMPOTENCY_KEY_CONFLICT: "The same idempotency-key was used with different parameters. Retry with the original parameters, or generate a NEW key for the new parameters."
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
var hotelCheckoutSchema = {
|
|
1419
|
+
cli: CLI_NAME,
|
|
1420
|
+
noun: HOTEL_NOUN,
|
|
1421
|
+
verb: "checkout",
|
|
1422
|
+
description: "Request a partial check-out or an out-of-policy cancellation (drop some nights, or cancel after the free window so the property must approve). This is an APPLICATION: it returns a task_order_code synchronously, then the supplier decides asynchronously \u2014 poll get-checkout. Use cancel for a simple whole-order in-policy cancellation",
|
|
1423
|
+
flags: {
|
|
1424
|
+
"order-id": { type: "string", required: true, description: "Our order reference (coOrderCode) for the URL path (book.response.order_id)" },
|
|
1425
|
+
"fc-order-code": { type: "string", required: true, description: "Supplier order reference of the booking to change (book.response.fc_order_code)" },
|
|
1426
|
+
reason: { type: "string", required: true, description: "Why the partial check-out / out-of-policy cancellation is requested" },
|
|
1427
|
+
"checkout-rooms": {
|
|
1428
|
+
type: "string",
|
|
1429
|
+
required: true,
|
|
1430
|
+
description: "Which booked rooms to check out of, taken from the book response (book.response.rooms[]). room_index and guest_name come verbatim from book \u2014 do NOT ask the user for them",
|
|
1431
|
+
constraints: "JSON array of {room_index, guest_name, cancel_check_in_date}; else PARAM_INVALID. cancel_check_in_date is the check-in date of the night(s) being dropped (YYYY-MM-DD)"
|
|
1432
|
+
},
|
|
1433
|
+
"refund-type": { type: "int", required: false, default: 1, description: "Requested refund handling (upstream refund-type code; default to the standard refund flow when unsure)" },
|
|
1434
|
+
"idempotency-key": {
|
|
1435
|
+
type: "string",
|
|
1436
|
+
required: true,
|
|
1437
|
+
description: "Unique key forwarded verbatim as the Idempotency-Key header; never auto-generated. Reuse the SAME key when retrying the same application",
|
|
1438
|
+
constraints: "1-128 chars [A-Za-z0-9_-]"
|
|
1439
|
+
}
|
|
1440
|
+
},
|
|
1441
|
+
response: {
|
|
1442
|
+
order_id: { type: "string", description: "Our order reference (echoes the path order_id)." },
|
|
1443
|
+
task_order_code: { type: "string", description: "Check-out application id. Poll get-checkout --task-order-code to track approval and refund." },
|
|
1444
|
+
apply_status: { type: "string|null", description: "Initial application status (e.g. submitted / pending)." },
|
|
1445
|
+
checkout_status: { type: "string|absent", description: "'checkout_pending' \u2014 async; poll get-checkout for the outcome." }
|
|
1446
|
+
},
|
|
1447
|
+
example: {
|
|
1448
|
+
command: `agenzo-merchant-cli hotel-redaug checkout --order-id E2E1750000000 --fc-order-code FC1750000000 --reason "guest leaving early" --checkout-rooms '[{"room_index":"1","guest_name":"San Zhang","cancel_check_in_date":"2026-07-05"}]' --idempotency-key checkout-h1n2`,
|
|
1449
|
+
output_summary: "Returns task_order_code. Poll hotel-redaug get-checkout --task-order-code <code> until the application is approved/rejected/refunded."
|
|
1450
|
+
},
|
|
1451
|
+
error_recovery: {
|
|
1452
|
+
CHECKOUT_NOT_ALLOWED: "The order state does not permit a check-out application (e.g. not yet confirmed, or already completed). Surface to user; do NOT retry.",
|
|
1453
|
+
HOTEL_ORDER_NOT_FOUND: "Verify --order-id / --fc-order-code from the book response. Do NOT retry.",
|
|
1454
|
+
UPSTREAM_ERROR: "Transient upstream error. Retry once after 5s with the SAME idempotency-key.",
|
|
1455
|
+
PARAM_IDEMPOTENCY_KEY_REQUIRED: "Add a unique --idempotency-key derived from business intent and retry.",
|
|
1456
|
+
PARAM_IDEMPOTENCY_KEY_CONFLICT: "The same idempotency-key was used with different parameters. Retry with the original parameters, or generate a NEW key for the new parameters."
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
var hotelGetCheckoutSchema = {
|
|
1460
|
+
cli: CLI_NAME,
|
|
1461
|
+
noun: HOTEL_NOUN,
|
|
1462
|
+
verb: "get-checkout",
|
|
1463
|
+
description: "Poll the status and refund outcome of a check-out application created by checkout. The supplier decides asynchronously; with --watch, keep polling until a terminal refund_status",
|
|
1464
|
+
flags: {
|
|
1465
|
+
"task-order-code": { type: "string", required: true, description: "Check-out application id from checkout (checkout.response.task_order_code)" },
|
|
1466
|
+
watch: { type: "bool", required: false, default: false, description: "Poll until a terminal status, emitting one NDJSON line per update" },
|
|
1467
|
+
"watch-interval": { type: "int", required: false, default: 10, description: "Seconds between polls when --watch is set" },
|
|
1468
|
+
"watch-timeout": { type: "int", required: false, default: 600, description: "Max seconds to poll before giving up" }
|
|
1469
|
+
},
|
|
1470
|
+
response: {
|
|
1471
|
+
task_order_code: { type: "string", description: "Check-out application id." },
|
|
1472
|
+
refund_status: { type: "string", description: "Application/refund status: pending | approved | rejected | refunded." },
|
|
1473
|
+
refund: {
|
|
1474
|
+
type: "object|null",
|
|
1475
|
+
description: "Final refund once approved. Populated only once refund_status is approved or refunded.",
|
|
1476
|
+
properties: {
|
|
1477
|
+
amount: { type: "float", description: "Refunded amount in DECIMAL units." },
|
|
1478
|
+
currency: { type: "string", description: "ISO 4217 currency code." }
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
},
|
|
1482
|
+
example: {
|
|
1483
|
+
command: "agenzo-merchant-cli hotel-redaug get-checkout --task-order-code TASK1750000000",
|
|
1484
|
+
output_summary: "Returns refund_status. Poll every 10s until approved/rejected/refunded; refund appears once approved."
|
|
1485
|
+
},
|
|
1486
|
+
polling: {
|
|
1487
|
+
recommended_interval_seconds: 10,
|
|
1488
|
+
terminal_statuses: ["approved", "rejected", "refunded"],
|
|
1489
|
+
in_progress_statuses: ["pending"],
|
|
1490
|
+
field_availability: {
|
|
1491
|
+
refund: "Populated only once refund_status is approved or refunded."
|
|
1492
|
+
}
|
|
1493
|
+
},
|
|
1494
|
+
error_recovery: {
|
|
1495
|
+
CHECKOUT_TASK_NOT_FOUND: "Verify --task-order-code from the checkout response. Do NOT retry.",
|
|
1496
|
+
UPSTREAM_ERROR: "Transient upstream error. Retry once after 10s."
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
var hotelListOrdersSchema = {
|
|
1500
|
+
cli: CLI_NAME,
|
|
1501
|
+
noun: HOTEL_NOUN,
|
|
1502
|
+
verb: "list-orders",
|
|
1503
|
+
description: "List the developer's hotel orders with optional status filtering and pagination",
|
|
1504
|
+
flags: {
|
|
1505
|
+
status: { type: "string", required: false, description: "Filter by order status (e.g. PROCESSING / CONFIRMED / CANCELLED / COMPLETED). Omitted entirely when absent (all statuses)" },
|
|
1506
|
+
page: { type: "int", required: false, default: 1, description: "Page number", constraints: ">= 1" },
|
|
1507
|
+
"page-size": { type: "int", required: false, default: 20, description: "Items per page", constraints: ">= 1 (platform caps at 100 server-side)" }
|
|
1508
|
+
},
|
|
1509
|
+
response: {
|
|
1510
|
+
orders: {
|
|
1511
|
+
type: "array",
|
|
1512
|
+
description: "List of hotel orders (slim list-item shape).",
|
|
1513
|
+
items: {
|
|
1514
|
+
order_id: { type: "string", description: "Our order reference." },
|
|
1515
|
+
fc_order_code: { type: "string", description: "Supplier order reference." },
|
|
1516
|
+
status: { type: "string", description: "Current order status (string)." },
|
|
1517
|
+
provider: { type: "string", description: "Service provider ('redaug')." },
|
|
1518
|
+
check_in: { type: "string|null", description: "Check-in date YYYY-MM-DD." },
|
|
1519
|
+
check_out: { type: "string|null", description: "Check-out date YYYY-MM-DD." },
|
|
1520
|
+
room_num: { type: "int|null", description: "Number of rooms booked." },
|
|
1521
|
+
price_amount: { type: "float|null", description: "Total price in DECIMAL units (NOT cents)." },
|
|
1522
|
+
price_currency: { type: "string", description: "ISO 4217 currency code." },
|
|
1523
|
+
payment_status: { type: "string", description: "Payment status (e.g. 'ON_ACCOUNT')." },
|
|
1524
|
+
hotel_confirm_no: { type: "string|null", description: "Property confirmation number (once confirmed)." },
|
|
1525
|
+
cancellation_fee: { type: "float|null", description: "Cancellation fee for cancelled orders; null otherwise." },
|
|
1526
|
+
refund_amount: { type: "float|null", description: "Amount refunded on cancellation/checkout; null otherwise." },
|
|
1527
|
+
created_at: { type: "string", description: "Order creation time (ISO 8601)." },
|
|
1528
|
+
updated_at: { type: "string", description: "Last update time (ISO 8601)." }
|
|
1529
|
+
}
|
|
1530
|
+
},
|
|
1531
|
+
total: { type: "int", description: "Total matching orders." },
|
|
1532
|
+
page: { type: "int", description: "Current page." },
|
|
1533
|
+
page_size: { type: "int", description: "Items per page." }
|
|
1534
|
+
},
|
|
1535
|
+
example: {
|
|
1536
|
+
command: "agenzo-merchant-cli hotel-redaug list-orders --status CONFIRMED --page 1 --page-size 10",
|
|
1537
|
+
output_summary: "Returns a paginated list of hotel orders with status, dates and amount info."
|
|
1538
|
+
},
|
|
1539
|
+
error_recovery: {
|
|
1540
|
+
INTERNAL_ERROR: "Transient backend error. Retry once after a short delay.",
|
|
1541
|
+
PARAM_INVALID: "Ensure --page / --page-size are positive integers, then retry."
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
var hotelFindDestinationSchema = {
|
|
1545
|
+
cli: CLI_NAME,
|
|
1546
|
+
noun: HOTEL_NOUN,
|
|
1547
|
+
verb: "find-destination",
|
|
1548
|
+
description: "Search for a destination by keyword (city, landmark, airport, etc.). Returns a list of destinations whose destination_id can be passed to search --destination-id or hotel-filters --destination-id",
|
|
1549
|
+
flags: {
|
|
1550
|
+
keyword: { type: "string", required: true, description: "Destination search keyword (city name, landmark, airport code, etc.)" },
|
|
1551
|
+
"data-type": { type: "string", required: false, description: "Optional data type filter to narrow result categories" }
|
|
1552
|
+
},
|
|
1553
|
+
response: {
|
|
1554
|
+
destinations: {
|
|
1555
|
+
type: "array",
|
|
1556
|
+
description: "Matching destinations. Empty when nothing matches the keyword (rendered as a successful empty result, not an error).",
|
|
1557
|
+
items: {
|
|
1558
|
+
destination_id: { type: "string|null", description: "Opaque destination identifier. Pass to search --destination-id or hotel-filters --destination-id." },
|
|
1559
|
+
type: { type: "string|int|null", description: "Destination type (city, landmark, airport, etc.)." },
|
|
1560
|
+
name: { type: "string|null", description: "Destination name." },
|
|
1561
|
+
city_name: { type: "string|null", description: "City name." },
|
|
1562
|
+
city_code: { type: "string|null", description: "City code." },
|
|
1563
|
+
country_name: { type: "string|null", description: "Country name." },
|
|
1564
|
+
lat: { type: "float|null", description: "Latitude (can also be used with search --lat)." },
|
|
1565
|
+
lng: { type: "float|null", description: "Longitude (can also be used with search --lng)." }
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
},
|
|
1569
|
+
example: {
|
|
1570
|
+
command: 'agenzo-merchant-cli hotel-redaug find-destination --keyword "Shanghai"',
|
|
1571
|
+
output_summary: "Returns destinations[]. Use destinations[].destination_id in search --destination-id or hotel-filters --destination-id."
|
|
1572
|
+
},
|
|
1573
|
+
error_recovery: {
|
|
1574
|
+
UPSTREAM_ERROR: "Transient upstream error. Retry once after ~2s backoff.",
|
|
1575
|
+
PARAM_INVALID: "Ensure --keyword is non-empty, then retry."
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1578
|
+
var hotelFiltersSchema = {
|
|
1579
|
+
cli: CLI_NAME,
|
|
1580
|
+
noun: HOTEL_NOUN,
|
|
1581
|
+
verb: "hotel-filters",
|
|
1582
|
+
description: "Get available filter options (stars, brands, groups, labels, sub-categories, hotel/room facilities) for a location. Each item's code maps directly to the corresponding search filter flag (e.g. hotel_facilities[].code \u2192 search --hotel-facility-codes)",
|
|
1583
|
+
flags: {
|
|
1584
|
+
"destination-id": { type: "string", required: "conditional", description: "Destination ID (alternative to --lat/--lng)", constraints: "Exactly one location branch: --destination-id XOR (--lat + --lng)" },
|
|
1585
|
+
lat: { type: "float", required: "conditional", description: "Latitude (-90 to 90, requires --lng)", constraints: "-90 to 90; mutually exclusive with --destination-id" },
|
|
1586
|
+
lng: { type: "float", required: "conditional", description: "Longitude (-180 to 180, requires --lat)", constraints: "-180 to 180; mutually exclusive with --destination-id" },
|
|
1587
|
+
distance: { type: "int", required: false, default: 20, description: "Search radius in kilometers" }
|
|
1588
|
+
},
|
|
1589
|
+
response: {
|
|
1590
|
+
stars: { type: "array", description: "Star rating options [{code, name, count}]. code \u2192 search --star." },
|
|
1591
|
+
brands: { type: "array", description: "Hotel brand options [{code, name, count}]. code \u2192 search --hotel-brand-codes." },
|
|
1592
|
+
groups: { type: "array", description: "Hotel group options [{code, name, count}]." },
|
|
1593
|
+
labels: { type: "array", description: "Hotel label options [{code, name, count}]. code \u2192 search --hotel-label-ids." },
|
|
1594
|
+
sub_categories: { type: "array", description: "Hotel sub-category options [{code, name, count}]. code \u2192 search --hotel-sub-category-ids." },
|
|
1595
|
+
hotel_facilities: { type: "array", description: "Hotel facility options [{code, name, count, type}]. code \u2192 search --hotel-facility-codes." },
|
|
1596
|
+
room_facilities: { type: "array", description: "Room facility options [{code, name, count, type}]. code \u2192 search --room-facility-codes." }
|
|
1597
|
+
},
|
|
1598
|
+
example: {
|
|
1599
|
+
command: "agenzo-merchant-cli hotel-redaug hotel-filters --destination-id D12345",
|
|
1600
|
+
output_summary: "Returns filter groups. Use the code values in search filter flags (e.g. search --hotel-facility-codes code1,code2)."
|
|
1601
|
+
},
|
|
1602
|
+
error_recovery: {
|
|
1603
|
+
UPSTREAM_ERROR: "Transient upstream error. Retry once after ~2s backoff.",
|
|
1604
|
+
PARAM_INVALID: "Supply exactly one location branch (--destination-id OR --lat/--lng). Coordinates must be within valid ranges."
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
var hotelListCitiesSchema = {
|
|
1608
|
+
cli: CLI_NAME,
|
|
1609
|
+
noun: HOTEL_NOUN,
|
|
1610
|
+
verb: "list-cities",
|
|
1611
|
+
description: "List cities available for hotel booking in a given country. Each city provides a destination_id (for search --destination-id) and/or lat/lng coordinates (for search --lat/--lng)",
|
|
1612
|
+
flags: {
|
|
1613
|
+
country: { type: "string", required: true, description: "ISO country code (e.g. CN, US, TH)" }
|
|
1614
|
+
},
|
|
1615
|
+
response: {
|
|
1616
|
+
cities: {
|
|
1617
|
+
type: "array",
|
|
1618
|
+
description: "Cities for the country. Empty when no cities are available (rendered as success, not an error).",
|
|
1619
|
+
items: {
|
|
1620
|
+
city_code: { type: "string", description: "City code." },
|
|
1621
|
+
city_name: { type: "string", description: "City name." },
|
|
1622
|
+
destination_id: { type: "string|null", description: "Destination ID for this city. Pass to search --destination-id." },
|
|
1623
|
+
province_code: { type: "string|null", description: "Province code." },
|
|
1624
|
+
province_name: { type: "string|null", description: "Province name." },
|
|
1625
|
+
country_code: { type: "string|null", description: "Country code." },
|
|
1626
|
+
country_name: { type: "string|null", description: "Country name." },
|
|
1627
|
+
lat: { type: "float|null", description: "Latitude (can be used with search --lat)." },
|
|
1628
|
+
lng: { type: "float|null", description: "Longitude (can be used with search --lng)." },
|
|
1629
|
+
time_zone: { type: "string|null", description: "Time zone identifier." },
|
|
1630
|
+
popularity_score: { type: "float|null", description: "Popularity score for sorting/ranking." }
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
},
|
|
1634
|
+
example: {
|
|
1635
|
+
command: "agenzo-merchant-cli hotel-redaug list-cities --country CN",
|
|
1636
|
+
output_summary: "Returns cities[]. Use cities[].destination_id in search --destination-id, or cities[].lat/lng in search --lat/--lng."
|
|
1637
|
+
},
|
|
1638
|
+
error_recovery: {
|
|
1639
|
+
UPSTREAM_ERROR: "Transient upstream error. Retry once after ~2s backoff.",
|
|
1640
|
+
PARAM_INVALID: "Ensure --country is a non-empty ISO country code, then retry."
|
|
1641
|
+
}
|
|
1642
|
+
};
|
|
1643
|
+
var hotelDetailSchema = {
|
|
1644
|
+
cli: CLI_NAME,
|
|
1645
|
+
noun: HOTEL_NOUN,
|
|
1646
|
+
verb: "hotel-detail",
|
|
1647
|
+
description: "Get detailed information about a specific hotel including facilities, images, location, check-in/check-out times. Use to show the user hotel details before quoting/booking",
|
|
1648
|
+
flags: {
|
|
1649
|
+
"hotel-id": { type: "string", required: true, description: "Hotel ID from search results (search.response.hotels[].hotel_id)" },
|
|
1650
|
+
"with-images": { type: "bool", required: false, default: true, description: "Include hotel images in the response (set false to reduce payload)" }
|
|
1651
|
+
},
|
|
1652
|
+
response: {
|
|
1653
|
+
hotel_id: { type: "string|int", description: "Hotel identifier." },
|
|
1654
|
+
hotel_name: { type: "string|null", description: "Hotel name." },
|
|
1655
|
+
hotel_eng_name: { type: "string|null", description: "English hotel name." },
|
|
1656
|
+
star: { type: "int|null", description: "Star rating." },
|
|
1657
|
+
address: { type: "string|null", description: "Hotel address." },
|
|
1658
|
+
intro: { type: "string|null", description: "Hotel introduction/description." },
|
|
1659
|
+
appearance_image: { type: "string|null", description: "Main appearance image URL." },
|
|
1660
|
+
telephone: { type: "string|null", description: "Hotel telephone number." },
|
|
1661
|
+
country_name: { type: "string|null", description: "Country name." },
|
|
1662
|
+
province_name: { type: "string|null", description: "Province name." },
|
|
1663
|
+
city_name: { type: "string|null", description: "City name." },
|
|
1664
|
+
district_name: { type: "string|null", description: "District name." },
|
|
1665
|
+
business_name: { type: "string|null", description: "Business area name." },
|
|
1666
|
+
lat: { type: "float|null", description: "Latitude." },
|
|
1667
|
+
lng: { type: "float|null", description: "Longitude." },
|
|
1668
|
+
check_in_time: { type: "string|null", description: "Hotel check-in time." },
|
|
1669
|
+
check_out_time: { type: "string|null", description: "Hotel check-out time." },
|
|
1670
|
+
room_num: { type: "int|null", description: "Number of rooms available." },
|
|
1671
|
+
facilities: {
|
|
1672
|
+
type: "array",
|
|
1673
|
+
description: "Hotel facilities list.",
|
|
1674
|
+
items: {
|
|
1675
|
+
name: { type: "string", description: "Facility name." },
|
|
1676
|
+
type: { type: "string|null", description: "Facility type/category." }
|
|
1677
|
+
}
|
|
1678
|
+
},
|
|
1679
|
+
images: {
|
|
1680
|
+
type: "array",
|
|
1681
|
+
description: "Hotel-level images. Empty array when --with-images false.",
|
|
1682
|
+
items: {
|
|
1683
|
+
url: { type: "string", description: "Image URL." },
|
|
1684
|
+
is_main: { type: "bool", description: "Whether this is the main image." },
|
|
1685
|
+
type: { type: "int|null", description: "Image type/category." }
|
|
1686
|
+
}
|
|
1687
|
+
},
|
|
1688
|
+
rooms: {
|
|
1689
|
+
type: "array",
|
|
1690
|
+
description: "Room types with STATIC info only (area/floor/beds/max occupancy/images) \u2014 NOT live rates. room_id is the SAME id space as quote's roomItems[].roomId, so relate a room here to its live rate/price/product_token by matching room_id to quote's rates. ALWAYS use quote for price/availability/product_token; this verb never has them. Present this alongside quote's rate options so the user sees what the room actually looks like (area, beds, photos) before picking a rate, not just the one-line room_name from quote.",
|
|
1691
|
+
items: {
|
|
1692
|
+
room_id: { type: "int|string|null", description: "Room type id. Matches quote's roomItems[].roomId \u2014 use to relate this room's detail to a specific quoted rate." },
|
|
1693
|
+
room_name: { type: "string|null", description: "Room type name." },
|
|
1694
|
+
area_sqm: { type: "string|null", description: "Room area (upstream unit, commonly square meters), as a string." },
|
|
1695
|
+
floor: { type: "string|null", description: 'Floor(s) this room type is on (may be a range, e.g. "10-15").' },
|
|
1696
|
+
max_person: { type: "int|null", description: "Max total occupancy." },
|
|
1697
|
+
max_adults: { type: "int|null", description: "Max adults." },
|
|
1698
|
+
max_child: { type: "int|null", description: "Max children." },
|
|
1699
|
+
allow_smoking: { type: "bool|null", description: "Whether smoking is allowed in this room type." },
|
|
1700
|
+
beds: {
|
|
1701
|
+
type: "array",
|
|
1702
|
+
description: "Bed configuration for the room area.",
|
|
1703
|
+
items: {
|
|
1704
|
+
name: { type: "string|null", description: 'Bed type name (e.g. "King Bed").' },
|
|
1705
|
+
width: { type: "string|null", description: "Bed width, upstream unit." },
|
|
1706
|
+
num: { type: "string|null", description: "Number of this bed type." }
|
|
1707
|
+
}
|
|
1708
|
+
},
|
|
1709
|
+
living_room_beds: {
|
|
1710
|
+
type: "array",
|
|
1711
|
+
description: "Bed configuration for a separate living-room area, when the room type has one. Same shape as beds.",
|
|
1712
|
+
items: {
|
|
1713
|
+
name: { type: "string|null", description: "Bed type name." },
|
|
1714
|
+
width: { type: "string|null", description: "Bed width, upstream unit." },
|
|
1715
|
+
num: { type: "string|null", description: "Number of this bed type." }
|
|
1716
|
+
}
|
|
1717
|
+
},
|
|
1718
|
+
images: {
|
|
1719
|
+
type: "array",
|
|
1720
|
+
description: "Photos of this specific room type. Empty when --with-images false, or when upstream has none for this room.",
|
|
1721
|
+
items: {
|
|
1722
|
+
url: { type: "string", description: "Image URL." },
|
|
1723
|
+
is_main: { type: "bool", description: "Whether this is the main image for the room." },
|
|
1724
|
+
type: { type: "int|null", description: "Image type/category." }
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
},
|
|
1730
|
+
example: {
|
|
1731
|
+
command: "agenzo-merchant-cli hotel-redaug hotel-detail --hotel-id 10583772",
|
|
1732
|
+
output_summary: "Returns full hotel details including facilities, hotel images, AND rooms[] (per-room-type static info + photos). Use to present both the hotel and its room types before quoting \u2014 rooms[].room_id matches quote's roomItems[].roomId so the two can be shown together."
|
|
1733
|
+
},
|
|
1734
|
+
error_recovery: {
|
|
1735
|
+
UPSTREAM_ERROR: "Transient upstream error. Retry once after ~2s backoff.",
|
|
1736
|
+
PARAM_INVALID: "Ensure --hotel-id is a non-empty string, then retry."
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1074
1739
|
|
|
1075
1740
|
// src/ride-elife/quote.ts
|
|
1076
1741
|
function need(value, flag) {
|
|
@@ -1221,6 +1886,17 @@ function num2(value, flag) {
|
|
|
1221
1886
|
}
|
|
1222
1887
|
return n;
|
|
1223
1888
|
}
|
|
1889
|
+
function phone(value, flag) {
|
|
1890
|
+
const raw = need2(value, flag);
|
|
1891
|
+
const cleaned = raw.replace(/[\s-]/g, "");
|
|
1892
|
+
if (!/^\+[1-9]\d{7,14}$/.test(cleaned)) {
|
|
1893
|
+
throw new CliError(
|
|
1894
|
+
"PARAM_INVALID",
|
|
1895
|
+
`--${flag} must be a valid international phone number, e.g. +14155552671.`
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
return cleaned;
|
|
1899
|
+
}
|
|
1224
1900
|
function seatCount(value, flag) {
|
|
1225
1901
|
const n = num2(value, flag);
|
|
1226
1902
|
if (!Number.isInteger(n) || n < 0 || n > 5) {
|
|
@@ -1266,7 +1942,7 @@ function registerBookCommand(parent, deps) {
|
|
|
1266
1942
|
price_amount: num2(opts.priceAmount, "price-amount"),
|
|
1267
1943
|
price_currency: opts.priceCurrency ?? "USD",
|
|
1268
1944
|
passenger_name: need2(opts.passengerName, "passenger-name"),
|
|
1269
|
-
passenger_phone:
|
|
1945
|
+
passenger_phone: phone(opts.passengerPhone, "passenger-phone")
|
|
1270
1946
|
};
|
|
1271
1947
|
if (opts.paymentOrderId) body.payment_order_id = opts.paymentOrderId;
|
|
1272
1948
|
if (opts.passengerEmail) body.passenger_email = opts.passengerEmail;
|
|
@@ -1384,8 +2060,8 @@ function formatGetOrder(data) {
|
|
|
1384
2060
|
}
|
|
1385
2061
|
if (data.driver) {
|
|
1386
2062
|
const driver = data.driver;
|
|
1387
|
-
const
|
|
1388
|
-
lines.push(["Driver", `${driver.name ?? "-"}${
|
|
2063
|
+
const phone2 = driver.phone_number ? ` (${driver.phone_number})` : "";
|
|
2064
|
+
lines.push(["Driver", `${driver.name ?? "-"}${phone2}`]);
|
|
1389
2065
|
}
|
|
1390
2066
|
if (data.vehicle) {
|
|
1391
2067
|
const v = data.vehicle;
|
|
@@ -1589,7 +2265,10 @@ function registerListOrdersCommand(parent, deps) {
|
|
|
1589
2265
|
// src/services/registry.ts
|
|
1590
2266
|
var SERVICE_REGISTRY = [
|
|
1591
2267
|
{
|
|
1592
|
-
service_id
|
|
2268
|
+
// service_id 是平台级不透明内部引用(svc_ 前缀 + ULID),不进 Agent 日常拼的命令
|
|
2269
|
+
// ——Agent 拼命令用的是 cli_noun="ride-elife"。与后端
|
|
2270
|
+
// services/merchant_service/discovery/catalog.py 的 RIDE_ELIFE_SERVICE_ID 保持一致。
|
|
2271
|
+
service_id: "svc_01KWTPBWDH5YZ52H717BFVWZA8",
|
|
1593
2272
|
name: "Ride hailing (eLife)",
|
|
1594
2273
|
description: "On-demand ride ordering: quote a fare, book it, poll status, and cancel.",
|
|
1595
2274
|
category: "ride",
|
|
@@ -1607,32 +2286,129 @@ var SERVICE_REGISTRY = [
|
|
|
1607
2286
|
workflow: ["quote", "book", "get (poll for status)", "cancel (optional)"],
|
|
1608
2287
|
since: "2026-06-01",
|
|
1609
2288
|
discovery: { help_command: "agenzo-merchant-cli ride-elife --help" }
|
|
2289
|
+
},
|
|
2290
|
+
{
|
|
2291
|
+
service_id: "svc_01J0HT5REDAUG0001",
|
|
2292
|
+
name: "Hotel booking (Redaug)",
|
|
2293
|
+
description: "International hotel booking via Redaug: search by location, real-time room/rate availability, two-step create-then-pay flow supporting monthly_settlement and pay_per_call (EVO) billing modes, cancel and partial check-out.",
|
|
2294
|
+
category: "hotel",
|
|
2295
|
+
version: "v1",
|
|
2296
|
+
provider: "redaug",
|
|
2297
|
+
cli_noun: "hotel-redaug",
|
|
2298
|
+
verbs: [
|
|
2299
|
+
"find-destination",
|
|
2300
|
+
"hotel-filters",
|
|
2301
|
+
"list-cities",
|
|
2302
|
+
"search",
|
|
2303
|
+
"hotel-detail",
|
|
2304
|
+
"quote",
|
|
2305
|
+
"create-order",
|
|
2306
|
+
"pay-order",
|
|
2307
|
+
"get",
|
|
2308
|
+
"cancel",
|
|
2309
|
+
"checkout",
|
|
2310
|
+
"get-checkout",
|
|
2311
|
+
"list-orders"
|
|
2312
|
+
],
|
|
2313
|
+
verb_descriptions: {
|
|
2314
|
+
"find-destination": "Resolve a free-text place into destinations.",
|
|
2315
|
+
"hotel-filters": "Get available filter options for a location.",
|
|
2316
|
+
"list-cities": "List the cities of a country.",
|
|
2317
|
+
search: "Search hotels by destination or coordinates.",
|
|
2318
|
+
"hotel-detail": "Get hotel detail (name/star/address/facilities/photos).",
|
|
2319
|
+
quote: "Get real-time room types and bookable rates for a hotel.",
|
|
2320
|
+
"create-order": "Create a hotel order without charging (locks the rate, no payment).",
|
|
2321
|
+
"pay-order": "Settle a created order by --order-id (path chosen server-side by billing_mode: monthly_settlement or pay_per_call).",
|
|
2322
|
+
get: "Query order status (poll for async confirmation).",
|
|
2323
|
+
cancel: "Cancel an order (whole-order, in-policy).",
|
|
2324
|
+
checkout: "Apply for partial check-out / out-of-policy cancellation.",
|
|
2325
|
+
"get-checkout": "Poll a check-out application status.",
|
|
2326
|
+
"list-orders": "List hotel orders (local store, no upstream call)."
|
|
2327
|
+
},
|
|
2328
|
+
workflow: [
|
|
2329
|
+
"find-destination / list-cities (optional)",
|
|
2330
|
+
"hotel-filters (optional)",
|
|
2331
|
+
"search",
|
|
2332
|
+
"hotel-detail (optional)",
|
|
2333
|
+
"quote",
|
|
2334
|
+
"create-order (locks the rate, no charge)",
|
|
2335
|
+
"pay-order (settles: monthly_settlement or pay_per_call)",
|
|
2336
|
+
"get (poll until CONFIRMED)",
|
|
2337
|
+
"cancel / checkout (optional)"
|
|
2338
|
+
],
|
|
2339
|
+
since: "2026-06-25",
|
|
2340
|
+
discovery: { help_command: "agenzo-merchant-cli hotel-redaug --help" }
|
|
1610
2341
|
}
|
|
1611
2342
|
];
|
|
1612
2343
|
function findService(serviceId) {
|
|
1613
2344
|
return SERVICE_REGISTRY.find((s) => s.service_id === serviceId);
|
|
1614
2345
|
}
|
|
1615
2346
|
|
|
2347
|
+
// src/services/local-caps.ts
|
|
2348
|
+
var NON_SERVICE_NOUNS = /* @__PURE__ */ new Set(["services", "help", "config"]);
|
|
2349
|
+
function buildLocalCapabilityMap(program) {
|
|
2350
|
+
const map = {};
|
|
2351
|
+
for (const noun of program.commands) {
|
|
2352
|
+
const name = noun.name();
|
|
2353
|
+
if (NON_SERVICE_NOUNS.has(name)) continue;
|
|
2354
|
+
const verbs = noun.commands.map((v) => v.name()).filter((n) => n !== "help");
|
|
2355
|
+
if (verbs.length === 0) continue;
|
|
2356
|
+
map[name] = verbs;
|
|
2357
|
+
}
|
|
2358
|
+
return map;
|
|
2359
|
+
}
|
|
2360
|
+
|
|
1616
2361
|
// src/services/list.ts
|
|
1617
2362
|
function toListItem(s) {
|
|
1618
2363
|
return {
|
|
1619
|
-
service_id: s.service_id,
|
|
1620
|
-
name: s.name,
|
|
1621
|
-
category: s.category,
|
|
1622
|
-
provider: s.provider,
|
|
1623
|
-
cli_noun: s.cli_noun,
|
|
1624
|
-
version: s.version,
|
|
1625
|
-
verbs: s.verbs,
|
|
1626
|
-
since: s.since,
|
|
2364
|
+
service_id: String(s.service_id ?? ""),
|
|
2365
|
+
name: String(s.name ?? ""),
|
|
2366
|
+
category: String(s.category ?? ""),
|
|
2367
|
+
provider: String(s.provider ?? ""),
|
|
2368
|
+
cli_noun: String(s.cli_noun ?? ""),
|
|
2369
|
+
version: String(s.version ?? ""),
|
|
2370
|
+
verbs: Array.isArray(s.verbs) ? s.verbs : [],
|
|
2371
|
+
since: String(s.since ?? ""),
|
|
1627
2372
|
discovery: s.discovery
|
|
1628
2373
|
};
|
|
1629
2374
|
}
|
|
1630
|
-
function
|
|
1631
|
-
const
|
|
2375
|
+
function gateByLocalCommands(capabilities, localCaps) {
|
|
2376
|
+
const gated = [];
|
|
2377
|
+
for (const cap of capabilities) {
|
|
2378
|
+
const localVerbs = localCaps[cap.cli_noun];
|
|
2379
|
+
if (!localVerbs) continue;
|
|
2380
|
+
const usableVerbs = (cap.verbs || []).filter((v) => localVerbs.includes(v));
|
|
2381
|
+
if (usableVerbs.length === 0) continue;
|
|
2382
|
+
gated.push({ ...cap, verbs: usableVerbs });
|
|
2383
|
+
}
|
|
2384
|
+
return gated;
|
|
2385
|
+
}
|
|
2386
|
+
function registerServicesListCommand(parent, deps) {
|
|
2387
|
+
const cmd = parent.command("list").description("List available merchant services (from platform discovery)").option("--api-key <key>", "API Key for authentication (X-Api-Key)");
|
|
1632
2388
|
cmd.action(async () => {
|
|
1633
2389
|
const opts = cmd.optsWithGlobals();
|
|
1634
2390
|
const format = resolveFormat(opts.format);
|
|
1635
|
-
const
|
|
2391
|
+
const localCaps = buildLocalCapabilityMap(deps.program);
|
|
2392
|
+
let services;
|
|
2393
|
+
try {
|
|
2394
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
2395
|
+
message: "API Key:",
|
|
2396
|
+
type: "password"
|
|
2397
|
+
});
|
|
2398
|
+
const result = await deps.discoveryClient.get("/api/discovery/v1/catalog", { type: "api-key", key: apiKey });
|
|
2399
|
+
if (!result.success) {
|
|
2400
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
2401
|
+
}
|
|
2402
|
+
services = gateByLocalCommands(
|
|
2403
|
+
(result.data.capabilities || []).map(toListItem),
|
|
2404
|
+
localCaps
|
|
2405
|
+
);
|
|
2406
|
+
} catch {
|
|
2407
|
+
services = gateByLocalCommands(
|
|
2408
|
+
SERVICE_REGISTRY.map((s) => toListItem(s)),
|
|
2409
|
+
localCaps
|
|
2410
|
+
);
|
|
2411
|
+
}
|
|
1636
2412
|
const configManager = new ConfigManager();
|
|
1637
2413
|
const commandResult = {
|
|
1638
2414
|
data: { services },
|
|
@@ -1657,46 +2433,1732 @@ function registerServicesListCommand(parent) {
|
|
|
1657
2433
|
}
|
|
1658
2434
|
|
|
1659
2435
|
// src/services/get.ts
|
|
2436
|
+
function buildVerbsSummary(data) {
|
|
2437
|
+
const verbs = Array.isArray(data.verbs) ? data.verbs : [];
|
|
2438
|
+
const verbDescriptions = data.verb_descriptions ?? {};
|
|
2439
|
+
const schema = data.schema_content;
|
|
2440
|
+
const schemaVerbs = schema?.verbs;
|
|
2441
|
+
return verbs.map((verb) => {
|
|
2442
|
+
const item = {
|
|
2443
|
+
verb,
|
|
2444
|
+
description: verbDescriptions[verb] ?? String(schemaVerbs?.[verb]?.description ?? "")
|
|
2445
|
+
};
|
|
2446
|
+
const annotations = schemaVerbs?.[verb]?.annotations;
|
|
2447
|
+
if (annotations) item.annotations = annotations;
|
|
2448
|
+
return item;
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
function toServiceLayerView(data) {
|
|
2452
|
+
const schema = data.schema_content;
|
|
2453
|
+
const view = {
|
|
2454
|
+
service_id: data.service_id,
|
|
2455
|
+
cli_noun: data.cli_noun,
|
|
2456
|
+
category: data.category,
|
|
2457
|
+
name: data.name,
|
|
2458
|
+
summary: data.description ?? schema?.summary,
|
|
2459
|
+
status: data.status ?? "available",
|
|
2460
|
+
provider: data.provider,
|
|
2461
|
+
version: data.version
|
|
2462
|
+
};
|
|
2463
|
+
if (schema?.selection_hints) view.selection_hints = schema.selection_hints;
|
|
2464
|
+
const discovery = data.discovery;
|
|
2465
|
+
view.schema_ref = schema?.schema_ref ?? {
|
|
2466
|
+
help_command: discovery?.help_command,
|
|
2467
|
+
...discovery?.schema_url ? { schema_url: discovery.schema_url } : {}
|
|
2468
|
+
};
|
|
2469
|
+
if (schema?.conventions) view.conventions = schema.conventions;
|
|
2470
|
+
if (schema?.workflow) {
|
|
2471
|
+
view.workflow = schema.workflow;
|
|
2472
|
+
} else if (Array.isArray(data.workflow)) {
|
|
2473
|
+
view.workflow = { description: data.workflow.join(" \u2192 ") };
|
|
2474
|
+
}
|
|
2475
|
+
view.verbs_summary = buildVerbsSummary(data);
|
|
2476
|
+
if (schema?.cross_service_recovery) view.cross_service_recovery = schema.cross_service_recovery;
|
|
2477
|
+
return view;
|
|
2478
|
+
}
|
|
1660
2479
|
function formatServiceGet(s) {
|
|
1661
2480
|
const lines = [
|
|
1662
|
-
["Service ID", s.service_id],
|
|
1663
|
-
["
|
|
1664
|
-
["
|
|
1665
|
-
["
|
|
1666
|
-
["
|
|
1667
|
-
["
|
|
1668
|
-
["
|
|
1669
|
-
["
|
|
1670
|
-
["Workflow", s.workflow.join(" \u2192 ")],
|
|
1671
|
-
["Since", s.since],
|
|
1672
|
-
["Help", s.discovery.help_command]
|
|
2481
|
+
["Service ID", String(s.service_id ?? "")],
|
|
2482
|
+
["CLI Noun", String(s.cli_noun ?? "")],
|
|
2483
|
+
["Category", String(s.category ?? "")],
|
|
2484
|
+
["Name", String(s.name ?? "")],
|
|
2485
|
+
["Summary", String(s.summary ?? "")],
|
|
2486
|
+
["Status", String(s.status ?? "")],
|
|
2487
|
+
["Provider", String(s.provider ?? "")],
|
|
2488
|
+
["Version", String(s.version ?? "")]
|
|
1673
2489
|
];
|
|
1674
|
-
const
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
2490
|
+
const block = Formatter.keyValue(lines);
|
|
2491
|
+
const sections = [];
|
|
2492
|
+
for (const key of [
|
|
2493
|
+
"selection_hints",
|
|
2494
|
+
"schema_ref",
|
|
2495
|
+
"conventions",
|
|
2496
|
+
"workflow",
|
|
2497
|
+
"verbs_summary",
|
|
2498
|
+
"cross_service_recovery"
|
|
2499
|
+
]) {
|
|
2500
|
+
if (s[key] !== void 0) {
|
|
2501
|
+
sections.push(`${key}:
|
|
2502
|
+
${JSON.stringify(s[key], null, 2)}`);
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
return sections.length > 0 ? `${block}
|
|
2506
|
+
|
|
2507
|
+
${sections.join("\n\n")}` : block;
|
|
1678
2508
|
}
|
|
1679
|
-
function
|
|
1680
|
-
const
|
|
2509
|
+
function gateCapability(data, localCaps) {
|
|
2510
|
+
const noun = String(data.cli_noun ?? "");
|
|
2511
|
+
const localVerbs = localCaps[noun];
|
|
2512
|
+
if (!localVerbs) return null;
|
|
2513
|
+
const out = { ...data };
|
|
2514
|
+
if (Array.isArray(out.verbs)) {
|
|
2515
|
+
out.verbs = out.verbs.filter((v) => localVerbs.includes(v));
|
|
2516
|
+
}
|
|
2517
|
+
const schema = out.schema_content;
|
|
2518
|
+
if (schema && schema.verbs && typeof schema.verbs === "object") {
|
|
2519
|
+
const filtered = {};
|
|
2520
|
+
for (const [verb, def] of Object.entries(schema.verbs)) {
|
|
2521
|
+
if (localVerbs.includes(verb)) filtered[verb] = def;
|
|
2522
|
+
}
|
|
2523
|
+
out.schema_content = { ...schema, verbs: filtered };
|
|
2524
|
+
}
|
|
2525
|
+
return out;
|
|
2526
|
+
}
|
|
2527
|
+
function registerServiceGetCommand(parent, deps) {
|
|
2528
|
+
const cmd = parent.command("get <service-id>").description("Retrieve a single merchant service (service-layer view: workflow + conventions + verbs_summary)").option("--api-key <key>", "API Key for authentication (X-Api-Key)");
|
|
1681
2529
|
cmd.action(async (serviceId) => {
|
|
1682
2530
|
const opts = cmd.optsWithGlobals();
|
|
1683
2531
|
const format = resolveFormat(opts.format);
|
|
1684
|
-
const
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
2532
|
+
const localCaps = buildLocalCapabilityMap(deps.program);
|
|
2533
|
+
let data = null;
|
|
2534
|
+
try {
|
|
2535
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
2536
|
+
message: "API Key:",
|
|
2537
|
+
type: "password"
|
|
2538
|
+
});
|
|
2539
|
+
const result = await deps.discoveryClient.get(
|
|
2540
|
+
`/api/discovery/v1/catalog/${encodeURIComponent(serviceId)}`,
|
|
2541
|
+
{ type: "api-key", key: apiKey }
|
|
1689
2542
|
);
|
|
2543
|
+
if (!result.success) {
|
|
2544
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
2545
|
+
}
|
|
2546
|
+
data = gateCapability(result.data, localCaps);
|
|
2547
|
+
} catch {
|
|
2548
|
+
const local = findService(serviceId);
|
|
2549
|
+
data = local ? gateCapability(local, localCaps) : null;
|
|
2550
|
+
}
|
|
2551
|
+
if (data === null) {
|
|
2552
|
+
throw new CliError(
|
|
2553
|
+
"SERVICE_NOT_FOUND",
|
|
2554
|
+
`Service '${serviceId}' is not available in this CLI. Run "services list" to see available services.`
|
|
2555
|
+
);
|
|
2556
|
+
}
|
|
2557
|
+
const resolved = toServiceLayerView(data);
|
|
2558
|
+
const configManager = new ConfigManager();
|
|
2559
|
+
const commandResult = {
|
|
2560
|
+
data: resolved,
|
|
2561
|
+
text: () => formatServiceGet(resolved)
|
|
2562
|
+
};
|
|
2563
|
+
await renderWithContext(commandResult, { format }, configManager);
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
// src/hotel-redaug/search.ts
|
|
2568
|
+
function need5(value, flag) {
|
|
2569
|
+
if (value === void 0) {
|
|
2570
|
+
throw new CliError("PARAM_INVALID", `Missing required --${flag}.`);
|
|
2571
|
+
}
|
|
2572
|
+
return value;
|
|
2573
|
+
}
|
|
2574
|
+
function num3(value, flag) {
|
|
2575
|
+
const n = Number(need5(value, flag));
|
|
2576
|
+
if (!Number.isFinite(n)) {
|
|
2577
|
+
throw new CliError("PARAM_INVALID", `--${flag} must be a number.`);
|
|
2578
|
+
}
|
|
2579
|
+
return n;
|
|
2580
|
+
}
|
|
2581
|
+
function coord(value, flag, min, max) {
|
|
2582
|
+
const n = num3(value, flag);
|
|
2583
|
+
if (n < min || n > max) {
|
|
2584
|
+
throw new CliError("PARAM_INVALID", `--${flag} must be between ${min} and ${max}.`);
|
|
2585
|
+
}
|
|
2586
|
+
return n;
|
|
2587
|
+
}
|
|
2588
|
+
function positiveInt2(value, flag) {
|
|
2589
|
+
const n = num3(value, flag);
|
|
2590
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
2591
|
+
throw new CliError("PARAM_INVALID", `--${flag} must be a positive integer.`);
|
|
2592
|
+
}
|
|
2593
|
+
return n;
|
|
2594
|
+
}
|
|
2595
|
+
var YMD_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
2596
|
+
function stayDate(value, flag) {
|
|
2597
|
+
const raw = need5(value, flag);
|
|
2598
|
+
if (!YMD_RE.test(raw)) {
|
|
2599
|
+
throw new CliError("PARAM_INVALID", `--${flag} must be a YYYY-MM-DD date.`);
|
|
2600
|
+
}
|
|
2601
|
+
const [y, m, d] = raw.split("-").map(Number);
|
|
2602
|
+
const dt = new Date(Date.UTC(y, m - 1, d));
|
|
2603
|
+
if (dt.getUTCFullYear() !== y || dt.getUTCMonth() !== m - 1 || dt.getUTCDate() !== d) {
|
|
2604
|
+
throw new CliError("PARAM_INVALID", `--${flag} must be a valid calendar date (YYYY-MM-DD).`);
|
|
2605
|
+
}
|
|
2606
|
+
return raw;
|
|
2607
|
+
}
|
|
2608
|
+
function formatSearch(data) {
|
|
2609
|
+
const hotels = data.hotels ?? [];
|
|
2610
|
+
if (hotels.length === 0) {
|
|
2611
|
+
return Formatter.status("info", "No hotels found for this area and dates");
|
|
2612
|
+
}
|
|
2613
|
+
const headers = ["Hotel ID", "Name", "Star", "Distance (km)", "From", "Currency", "City", "Score", "Image", "Address"];
|
|
2614
|
+
const rows = hotels.map((h) => [
|
|
2615
|
+
String(h.hotel_id ?? "-"),
|
|
2616
|
+
String(h.hotel_name ?? "-"),
|
|
2617
|
+
String(h.star ?? "-"),
|
|
2618
|
+
String(h.distance_km ?? "-"),
|
|
2619
|
+
String(h.lowest_price?.amount ?? "-"),
|
|
2620
|
+
String(h.lowest_price?.currency ?? "-"),
|
|
2621
|
+
String(h.city_name ?? "-"),
|
|
2622
|
+
String(h.score ?? "-"),
|
|
2623
|
+
String(h.main_image ?? "-"),
|
|
2624
|
+
String(h.address ?? "-")
|
|
2625
|
+
]);
|
|
2626
|
+
return Formatter.table(headers, rows);
|
|
2627
|
+
}
|
|
2628
|
+
function registerHotelSearchCommand(parent, deps) {
|
|
2629
|
+
const cmd = parent.command("search").description("Search hotels near a coordinate or within a destination for the given stay dates").option("--destination-id <id>", "Destination ID (alternative to --lat/--lng)").option("--lat <lat>", "Latitude of the search center (-90 to 90)").option("--lng <lng>", "Longitude of the search center (-180 to 180)").option("--distance <km>", "Search radius in kilometers", "20").option("--check-in <date>", "Check-in date (YYYY-MM-DD)").option("--check-out <date>", "Check-out date (YYYY-MM-DD, strictly after check-in)").option("--adults <n>", "Number of adults", "2").option("--children <n>", "Number of children", "0").option("--room-num <n>", "Number of rooms requested", "1").option("--star <n>", "Minimum hotel star rating filter (3/4/5)").option("--keyword <text>", "Optional hotel name keyword to narrow results").option("--price-min <n>", "Minimum price filter").option("--price-max <n>", "Maximum price filter").option("--sort-by <field>", "Sort field").option("--page <n>", "Page number (positive integer)", "1").option("--page-size <n>", "Results per page (positive integer)", "10").option("--hotel-facility-codes <codes>", "Hotel facility codes (comma-separated or JSON array)").option("--room-facility-codes <codes>", "Room facility codes (comma-separated or JSON array)").option("--hotel-brand-codes <codes>", "Hotel brand codes (comma-separated or JSON array)").option("--plate-codes <codes>", "Plate codes (comma-separated or JSON array)").option("--hotel-label-ids <ids>", "Hotel label IDs (comma-separated or JSON array)").option("--hotel-sub-category-ids <ids>", "Hotel sub-category IDs (comma-separated or JSON array)").option("--api-key <key>", "API Key for authentication (X-Api-Key)");
|
|
2630
|
+
attachSchemaHelp(cmd, hotelSearchSchema);
|
|
2631
|
+
cmd.action(async () => {
|
|
2632
|
+
const opts = cmd.optsWithGlobals();
|
|
2633
|
+
const format = resolveFormat(opts.format);
|
|
2634
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
2635
|
+
message: "API Key:",
|
|
2636
|
+
type: "password"
|
|
2637
|
+
});
|
|
2638
|
+
const hasDestination = opts.destinationId !== void 0;
|
|
2639
|
+
const hasLat = opts.lat !== void 0;
|
|
2640
|
+
const hasLng = opts.lng !== void 0;
|
|
2641
|
+
const hasCoordBranch = hasLat || hasLng;
|
|
2642
|
+
if (hasDestination && hasCoordBranch) {
|
|
2643
|
+
throw new CliError(
|
|
2644
|
+
"PARAM_INVALID",
|
|
2645
|
+
"Supply either --destination-id OR --lat/--lng, not both."
|
|
2646
|
+
);
|
|
2647
|
+
}
|
|
2648
|
+
if (!hasDestination && !hasCoordBranch) {
|
|
2649
|
+
throw new CliError(
|
|
2650
|
+
"PARAM_INVALID",
|
|
2651
|
+
"Supply either --destination-id OR --lat/--lng (one location branch is required)."
|
|
2652
|
+
);
|
|
2653
|
+
}
|
|
2654
|
+
if (hasCoordBranch && !(hasLat && hasLng)) {
|
|
2655
|
+
throw new CliError(
|
|
2656
|
+
"PARAM_INVALID",
|
|
2657
|
+
"--lat and --lng must both be provided together."
|
|
2658
|
+
);
|
|
2659
|
+
}
|
|
2660
|
+
const checkIn = stayDate(opts.checkIn, "check-in");
|
|
2661
|
+
const checkOut = stayDate(opts.checkOut, "check-out");
|
|
2662
|
+
if (!(checkOut > checkIn)) {
|
|
2663
|
+
throw new CliError("PARAM_INVALID", "--check-out must be strictly after --check-in.");
|
|
2664
|
+
}
|
|
2665
|
+
const page = positiveInt2(opts.page, "page");
|
|
2666
|
+
const pageSize = positiveInt2(opts.pageSize, "page-size");
|
|
2667
|
+
const body = {};
|
|
2668
|
+
if (hasDestination) {
|
|
2669
|
+
body.destination_id = need5(opts.destinationId, "destination-id");
|
|
2670
|
+
} else {
|
|
2671
|
+
body.lat = coord(opts.lat, "lat", -90, 90);
|
|
2672
|
+
body.lng = coord(opts.lng, "lng", -180, 180);
|
|
2673
|
+
}
|
|
2674
|
+
body.distance = num3(opts.distance, "distance");
|
|
2675
|
+
body.check_in = checkIn;
|
|
2676
|
+
body.check_out = checkOut;
|
|
2677
|
+
body.adults = num3(opts.adults, "adults");
|
|
2678
|
+
body.children = num3(opts.children, "children");
|
|
2679
|
+
body.room_num = num3(opts.roomNum, "room-num");
|
|
2680
|
+
if (opts.star !== void 0) body.star = num3(opts.star, "star");
|
|
2681
|
+
if (opts.keyword !== void 0) body.keyword = opts.keyword;
|
|
2682
|
+
if (opts.priceMin !== void 0) body.price_min = num3(opts.priceMin, "price-min");
|
|
2683
|
+
if (opts.priceMax !== void 0) body.price_max = num3(opts.priceMax, "price-max");
|
|
2684
|
+
if (opts.sortBy !== void 0) body.sort_by = opts.sortBy;
|
|
2685
|
+
body.page = page;
|
|
2686
|
+
body.page_size = pageSize;
|
|
2687
|
+
if (opts.hotelFacilityCodes !== void 0) body.hotel_facility_codes = opts.hotelFacilityCodes;
|
|
2688
|
+
if (opts.roomFacilityCodes !== void 0) body.room_facility_codes = opts.roomFacilityCodes;
|
|
2689
|
+
if (opts.hotelBrandCodes !== void 0) body.hotel_brand_codes = opts.hotelBrandCodes;
|
|
2690
|
+
if (opts.plateCodes !== void 0) body.plate_codes = opts.plateCodes;
|
|
2691
|
+
if (opts.hotelLabelIds !== void 0) body.hotel_label_ids = opts.hotelLabelIds;
|
|
2692
|
+
if (opts.hotelSubCategoryIds !== void 0) body.hotel_sub_category_ids = opts.hotelSubCategoryIds;
|
|
2693
|
+
const result = await deps.apiClient.post(
|
|
2694
|
+
"/hotel/search",
|
|
2695
|
+
{ type: "api-key", key: apiKey },
|
|
2696
|
+
body
|
|
2697
|
+
);
|
|
2698
|
+
if (!result.success) {
|
|
2699
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
2700
|
+
}
|
|
2701
|
+
const data = result.data;
|
|
2702
|
+
const configManager = new ConfigManager();
|
|
2703
|
+
const commandResult = {
|
|
2704
|
+
data,
|
|
2705
|
+
text: () => formatSearch(data)
|
|
2706
|
+
};
|
|
2707
|
+
await renderWithContext(commandResult, { format }, configManager);
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
// src/hotel-redaug/quote.ts
|
|
2712
|
+
function need6(value, flag) {
|
|
2713
|
+
if (value === void 0) {
|
|
2714
|
+
throw new CliError("PARAM_INVALID", `Missing required --${flag}.`);
|
|
2715
|
+
}
|
|
2716
|
+
return value;
|
|
2717
|
+
}
|
|
2718
|
+
function num4(value, flag) {
|
|
2719
|
+
const n = Number(need6(value, flag));
|
|
2720
|
+
if (!Number.isFinite(n)) {
|
|
2721
|
+
throw new CliError("PARAM_INVALID", `--${flag} must be a number.`);
|
|
2722
|
+
}
|
|
2723
|
+
return n;
|
|
2724
|
+
}
|
|
2725
|
+
var YMD_RE2 = /^\d{4}-\d{2}-\d{2}$/;
|
|
2726
|
+
function stayDate2(value, flag) {
|
|
2727
|
+
const raw = need6(value, flag);
|
|
2728
|
+
if (!YMD_RE2.test(raw)) {
|
|
2729
|
+
throw new CliError("PARAM_INVALID", `--${flag} must be a YYYY-MM-DD date.`);
|
|
2730
|
+
}
|
|
2731
|
+
const [y, m, d] = raw.split("-").map(Number);
|
|
2732
|
+
const dt = new Date(Date.UTC(y, m - 1, d));
|
|
2733
|
+
if (dt.getUTCFullYear() !== y || dt.getUTCMonth() !== m - 1 || dt.getUTCDate() !== d) {
|
|
2734
|
+
throw new CliError("PARAM_INVALID", `--${flag} must be a valid calendar date (YYYY-MM-DD).`);
|
|
2735
|
+
}
|
|
2736
|
+
return raw;
|
|
2737
|
+
}
|
|
2738
|
+
function summarizePriceItems(items) {
|
|
2739
|
+
if (!items || items.length === 0) return "-";
|
|
2740
|
+
return items.map((it) => `${it.sale_date}: ${it.sale_price} (bf ${it.breakfast_num})`).join("; ");
|
|
2741
|
+
}
|
|
2742
|
+
function formatQuote2(data) {
|
|
2743
|
+
const rates = data.rates ?? [];
|
|
2744
|
+
if (rates.length === 0) {
|
|
2745
|
+
return Formatter.status("info", "No bookable rates for this hotel and dates");
|
|
2746
|
+
}
|
|
2747
|
+
const headers = ["Product Token", "Room", "Total", "Currency", "Per-night (price_items)"];
|
|
2748
|
+
const rows = rates.map((r) => [
|
|
2749
|
+
String(r.product_token ?? "-"),
|
|
2750
|
+
String(r.room_name ?? "-"),
|
|
2751
|
+
String(r.total_price?.amount ?? "-"),
|
|
2752
|
+
String(r.total_price?.currency ?? "-"),
|
|
2753
|
+
summarizePriceItems(r.price_items)
|
|
2754
|
+
]);
|
|
2755
|
+
return Formatter.table(headers, rows);
|
|
2756
|
+
}
|
|
2757
|
+
function registerHotelQuoteCommand(parent, deps) {
|
|
2758
|
+
const cmd = parent.command("quote").description("Get real-time room types and bookable rates for one hotel and stay").option("--api-key <key>", "API Key for authentication (X-Api-Key)").option("--hotel-id <id>", "Hotel chosen from search results (hotels[].hotel_id)").option("--check-in <date>", "Check-in date (YYYY-MM-DD, match search)").option("--check-out <date>", "Check-out date (YYYY-MM-DD, strictly after check-in)").option("--adults <n>", "Number of adults per room", "2").option("--children <n>", "Number of children per room", "0").option("--room-num <n>", "Number of rooms requested", "1").option("--nationality <code>", "Guest nationality (ISO country code)", "CN");
|
|
2759
|
+
attachSchemaHelp(cmd, hotelQuoteSchema);
|
|
2760
|
+
cmd.action(async () => {
|
|
2761
|
+
const opts = cmd.optsWithGlobals();
|
|
2762
|
+
const format = resolveFormat(opts.format);
|
|
2763
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
2764
|
+
message: "API Key:",
|
|
2765
|
+
type: "password"
|
|
2766
|
+
});
|
|
2767
|
+
const hotelId = need6(opts.hotelId, "hotel-id");
|
|
2768
|
+
const checkIn = stayDate2(opts.checkIn, "check-in");
|
|
2769
|
+
const checkOut = stayDate2(opts.checkOut, "check-out");
|
|
2770
|
+
if (!(checkOut > checkIn)) {
|
|
2771
|
+
throw new CliError("PARAM_INVALID", "--check-out must be strictly after --check-in.");
|
|
2772
|
+
}
|
|
2773
|
+
const body = {
|
|
2774
|
+
hotel_id: hotelId,
|
|
2775
|
+
check_in: checkIn,
|
|
2776
|
+
check_out: checkOut,
|
|
2777
|
+
adults: num4(opts.adults, "adults"),
|
|
2778
|
+
children: num4(opts.children, "children"),
|
|
2779
|
+
room_num: num4(opts.roomNum, "room-num"),
|
|
2780
|
+
nationality: opts.nationality
|
|
2781
|
+
};
|
|
2782
|
+
const result = await deps.apiClient.post(
|
|
2783
|
+
"/hotel/quote",
|
|
2784
|
+
{ type: "api-key", key: apiKey },
|
|
2785
|
+
body
|
|
2786
|
+
);
|
|
2787
|
+
if (!result.success) {
|
|
2788
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
2789
|
+
}
|
|
2790
|
+
const data = result.data;
|
|
2791
|
+
const configManager = new ConfigManager();
|
|
2792
|
+
const commandResult = {
|
|
2793
|
+
data,
|
|
2794
|
+
text: () => formatQuote2(data)
|
|
2795
|
+
};
|
|
2796
|
+
await renderWithContext(commandResult, { format }, configManager);
|
|
2797
|
+
});
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
// src/hotel-redaug/create-order.ts
|
|
2801
|
+
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
2802
|
+
function parsePriceItems(raw) {
|
|
2803
|
+
const INVALID = "--price-items must be a JSON array of {sale_date (string), sale_price (number), breakfast_num (number)}.";
|
|
2804
|
+
let parsed;
|
|
2805
|
+
try {
|
|
2806
|
+
parsed = JSON.parse(raw);
|
|
2807
|
+
} catch {
|
|
2808
|
+
throw new CliError("PARAM_INVALID", INVALID);
|
|
2809
|
+
}
|
|
2810
|
+
if (!Array.isArray(parsed)) {
|
|
2811
|
+
throw new CliError("PARAM_INVALID", INVALID);
|
|
2812
|
+
}
|
|
2813
|
+
for (const el of parsed) {
|
|
2814
|
+
if (typeof el !== "object" || el === null || Array.isArray(el)) {
|
|
2815
|
+
throw new CliError("PARAM_INVALID", INVALID);
|
|
2816
|
+
}
|
|
2817
|
+
const item = el;
|
|
2818
|
+
if (typeof item.sale_date !== "string" || typeof item.sale_price !== "number" || typeof item.breakfast_num !== "number") {
|
|
2819
|
+
throw new CliError("PARAM_INVALID", INVALID);
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
return parsed;
|
|
2823
|
+
}
|
|
2824
|
+
function need7(value, flag) {
|
|
2825
|
+
if (value === void 0) {
|
|
2826
|
+
throw new CliError("PARAM_INVALID", `Missing required --${flag}.`);
|
|
2827
|
+
}
|
|
2828
|
+
return value;
|
|
2829
|
+
}
|
|
2830
|
+
function num5(value, flag) {
|
|
2831
|
+
const n = Number(need7(value, flag));
|
|
2832
|
+
if (!Number.isFinite(n)) {
|
|
2833
|
+
throw new CliError("PARAM_INVALID", `--${flag} must be a number.`);
|
|
2834
|
+
}
|
|
2835
|
+
return n;
|
|
2836
|
+
}
|
|
2837
|
+
var YMD_RE3 = /^\d{4}-\d{2}-\d{2}$/;
|
|
2838
|
+
function stayDate3(value, flag) {
|
|
2839
|
+
const raw = need7(value, flag);
|
|
2840
|
+
if (!YMD_RE3.test(raw)) {
|
|
2841
|
+
throw new CliError("PARAM_INVALID", `--${flag} must be a YYYY-MM-DD date.`);
|
|
2842
|
+
}
|
|
2843
|
+
const [y, m, d] = raw.split("-").map(Number);
|
|
2844
|
+
const dt = new Date(Date.UTC(y, m - 1, d));
|
|
2845
|
+
if (dt.getUTCFullYear() !== y || dt.getUTCMonth() !== m - 1 || dt.getUTCDate() !== d) {
|
|
2846
|
+
throw new CliError("PARAM_INVALID", `--${flag} must be a valid calendar date (YYYY-MM-DD).`);
|
|
2847
|
+
}
|
|
2848
|
+
return raw;
|
|
2849
|
+
}
|
|
2850
|
+
function formatCreateOrder(data) {
|
|
2851
|
+
const lines = [
|
|
2852
|
+
["Order ID", String(data.order_id ?? "-")],
|
|
2853
|
+
["FC order code", String(data.fc_order_code ?? "-")],
|
|
2854
|
+
["Order status", String(data.order_status ?? "-")],
|
|
2855
|
+
["Total amount", `${data.total_amount} ${data.currency}`]
|
|
2856
|
+
];
|
|
2857
|
+
const out = [Formatter.keyValue(lines)];
|
|
2858
|
+
const rooms = data.rooms ?? [];
|
|
2859
|
+
if (rooms.length > 0) {
|
|
2860
|
+
const headers = ["Room index", "Guest name"];
|
|
2861
|
+
const rows = rooms.map((r) => [String(r.room_index ?? "-"), String(r.guest_name ?? "-")]);
|
|
2862
|
+
out.push("", Formatter.table(headers, rows));
|
|
2863
|
+
}
|
|
2864
|
+
return out.join("\n");
|
|
2865
|
+
}
|
|
2866
|
+
function registerHotelCreateOrderCommand(parent, deps) {
|
|
2867
|
+
const cmd = parent.command("create-order").description("Create AND pay for a hotel order in one call (billing path decided server-side by billing_mode)").option("--api-key <key>", "API Key for authentication (X-Api-Key)").option("--product-token <token>", "Chosen rate token from quote (rates[].product_token)").option("--total-amount <amount>", "Total stay price in decimal currency units (not cents)").option("--currency <currency>", "ISO 4217 currency code from the chosen rate").option("--price-items <json>", "Per-night price breakdown JSON array (copied verbatim from quote)").option("--check-in <date>", "Check-in date (YYYY-MM-DD, match quote)").option("--check-out <date>", "Check-out date (YYYY-MM-DD, strictly after check-in)").option("--room-num <n>", "Number of rooms (match quote)", "1").option("--adults <n>", "Adults per room (match quote)", "2").option("--children <n>", "Children per room (match quote)", "0").option("--nationality <code>", "Guest nationality (ISO country code)", "CN").option("--guest-name <name>", "Primary guest name").option("--contact-name <name>", "Booking contact name (may equal guest-name)").option("--contact-phone <phone>", "Booking contact phone number (digits)").option("--contact-country-code <code>", "Phone country calling code without '+'", "86").option("--contact-email <email>", "Booking contact email").option("--arrive-time <time>", "Expected arrival time (HH:mm, hotel local time)").option("--special-requests <text>", "Free-text special requests (non-binding)").option(
|
|
2868
|
+
"--payment-method-id <id>",
|
|
2869
|
+
"pay_per_call only: charge this SPECIFIC already-bound card instead of the platform auto-selected one"
|
|
2870
|
+
).option(
|
|
2871
|
+
"--idempotency-key <key>",
|
|
2872
|
+
"Idempotency key forwarded verbatim as the Idempotency-Key header (covers both order creation AND payment)"
|
|
2873
|
+
);
|
|
2874
|
+
attachSchemaHelp(cmd, hotelCreateOrderSchema);
|
|
2875
|
+
cmd.action(async () => {
|
|
2876
|
+
const opts = cmd.optsWithGlobals();
|
|
2877
|
+
const format = resolveFormat(opts.format);
|
|
2878
|
+
const isYes = Boolean(opts.yes);
|
|
2879
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
2880
|
+
message: "API Key:",
|
|
2881
|
+
type: "password"
|
|
2882
|
+
});
|
|
2883
|
+
const productToken = need7(opts.productToken, "product-token");
|
|
2884
|
+
const totalAmount = num5(opts.totalAmount, "total-amount");
|
|
2885
|
+
const currency = need7(opts.currency, "currency");
|
|
2886
|
+
const priceItems = parsePriceItems(need7(opts.priceItems, "price-items"));
|
|
2887
|
+
const checkIn = stayDate3(opts.checkIn, "check-in");
|
|
2888
|
+
const checkOut = stayDate3(opts.checkOut, "check-out");
|
|
2889
|
+
if (!(checkOut > checkIn)) {
|
|
2890
|
+
throw new CliError("PARAM_INVALID", "--check-out must be strictly after --check-in.");
|
|
2891
|
+
}
|
|
2892
|
+
const guestName = need7(opts.guestName, "guest-name");
|
|
2893
|
+
const contactName = need7(opts.contactName, "contact-name");
|
|
2894
|
+
const contactPhone = need7(opts.contactPhone, "contact-phone");
|
|
2895
|
+
const body = {
|
|
2896
|
+
product_token: productToken,
|
|
2897
|
+
total_amount: totalAmount,
|
|
2898
|
+
currency,
|
|
2899
|
+
price_items: priceItems,
|
|
2900
|
+
check_in: checkIn,
|
|
2901
|
+
check_out: checkOut,
|
|
2902
|
+
room_num: num5(opts.roomNum, "room-num"),
|
|
2903
|
+
adults: num5(opts.adults, "adults"),
|
|
2904
|
+
children: num5(opts.children, "children"),
|
|
2905
|
+
nationality: opts.nationality,
|
|
2906
|
+
guest_name: guestName,
|
|
2907
|
+
contact_name: contactName,
|
|
2908
|
+
contact_phone: contactPhone,
|
|
2909
|
+
contact_country_code: opts.contactCountryCode
|
|
2910
|
+
};
|
|
2911
|
+
if (opts.contactEmail !== void 0) body.contact_email = opts.contactEmail;
|
|
2912
|
+
if (opts.arriveTime !== void 0) body.arrive_time = opts.arriveTime;
|
|
2913
|
+
if (opts.specialRequests !== void 0) body.special_requests = opts.specialRequests;
|
|
2914
|
+
if (opts.paymentMethodId !== void 0) body.payment_method_id = opts.paymentMethodId;
|
|
2915
|
+
if (!isYes) {
|
|
2916
|
+
const confirmed = await confirm3({
|
|
2917
|
+
message: `Create and pay for this hotel order: ${totalAmount} ${currency} (check-in ${checkIn}, check-out ${checkOut})? This locks the rate AND charges the amount immediately.`,
|
|
2918
|
+
default: false
|
|
2919
|
+
});
|
|
2920
|
+
if (!confirmed) {
|
|
2921
|
+
throw new CliError("CLIENT_ABORTED", "Order creation aborted by user.");
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
const idempotencyKey = await resolveIdempotencyKey(opts.idempotencyKey, {
|
|
2925
|
+
yes: isYes,
|
|
2926
|
+
commandPath: "hotel-redaug create-order"
|
|
2927
|
+
});
|
|
2928
|
+
const spinner = format === "json" ? null : createSpinner("Creating hotel order...");
|
|
2929
|
+
const result = await deps.apiClient.post(
|
|
2930
|
+
"/hotel/create-order",
|
|
2931
|
+
{ type: "api-key", key: apiKey },
|
|
2932
|
+
body,
|
|
2933
|
+
{ "Idempotency-Key": idempotencyKey }
|
|
2934
|
+
);
|
|
2935
|
+
spinner?.stop();
|
|
2936
|
+
if (!result.success) {
|
|
2937
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
2938
|
+
}
|
|
2939
|
+
const data = result.data;
|
|
2940
|
+
const configManager = new ConfigManager();
|
|
2941
|
+
const commandResult = {
|
|
2942
|
+
data,
|
|
2943
|
+
text: () => formatCreateOrder(data)
|
|
2944
|
+
};
|
|
2945
|
+
await renderWithContext(commandResult, { format }, configManager);
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
// src/hotel-redaug/pay-order.ts
|
|
2950
|
+
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
2951
|
+
|
|
2952
|
+
// src/hotel-redaug/watch.ts
|
|
2953
|
+
var DEFAULT_ORDER_WATCH_INTERVAL_SECONDS = 5;
|
|
2954
|
+
var DEFAULT_CHECKOUT_WATCH_INTERVAL_SECONDS = 10;
|
|
2955
|
+
var DEFAULT_WATCH_TIMEOUT_SECONDS2 = 600;
|
|
2956
|
+
var HOTEL_TERMINAL_ORDER_CODES = /* @__PURE__ */ new Set([3, 4, 5]);
|
|
2957
|
+
var HOTEL_TERMINAL_ORDER_STATUS = /* @__PURE__ */ new Set([
|
|
2958
|
+
"CONFIRMED",
|
|
2959
|
+
"CANCELLED",
|
|
2960
|
+
"COMPLETED"
|
|
2961
|
+
]);
|
|
2962
|
+
function orderStatusCodeOf(record) {
|
|
2963
|
+
const code = record.order_status_code;
|
|
2964
|
+
return typeof code === "number" ? code : void 0;
|
|
2965
|
+
}
|
|
2966
|
+
function orderStatusOf(record) {
|
|
2967
|
+
const status = record.order_status;
|
|
2968
|
+
return typeof status === "string" ? status : void 0;
|
|
2969
|
+
}
|
|
2970
|
+
function isTerminalOrderStatus(record) {
|
|
2971
|
+
const code = orderStatusCodeOf(record);
|
|
2972
|
+
if (code !== void 0 && HOTEL_TERMINAL_ORDER_CODES.has(code)) {
|
|
2973
|
+
return true;
|
|
2974
|
+
}
|
|
2975
|
+
const status = orderStatusOf(record);
|
|
2976
|
+
return status !== void 0 && HOTEL_TERMINAL_ORDER_STATUS.has(status);
|
|
2977
|
+
}
|
|
2978
|
+
function lastOrderStatusOf(record) {
|
|
2979
|
+
return orderStatusOf(record) ?? orderStatusCodeOf(record) ?? null;
|
|
2980
|
+
}
|
|
2981
|
+
var CHECKOUT_TERMINAL_REFUND = /* @__PURE__ */ new Set([
|
|
2982
|
+
"approved",
|
|
2983
|
+
"rejected",
|
|
2984
|
+
"refunded"
|
|
2985
|
+
]);
|
|
2986
|
+
function refundStatusOf(record) {
|
|
2987
|
+
const status = record.refund_status;
|
|
2988
|
+
return typeof status === "string" ? status : void 0;
|
|
2989
|
+
}
|
|
2990
|
+
function isTerminalRefundStatus(record) {
|
|
2991
|
+
const status = refundStatusOf(record);
|
|
2992
|
+
return status !== void 0 && CHECKOUT_TERMINAL_REFUND.has(status);
|
|
2993
|
+
}
|
|
2994
|
+
function lastRefundStatusOf(record) {
|
|
2995
|
+
return refundStatusOf(record) ?? null;
|
|
2996
|
+
}
|
|
2997
|
+
async function runHotelWatch(deps, opts) {
|
|
2998
|
+
const deadline = deps.now() + opts.timeoutMs;
|
|
2999
|
+
for (; ; ) {
|
|
3000
|
+
const record = await deps.fetchStatus();
|
|
3001
|
+
deps.writeLine(record);
|
|
3002
|
+
if (deps.isTerminal(record)) {
|
|
3003
|
+
return;
|
|
3004
|
+
}
|
|
3005
|
+
if (deps.now() + opts.intervalMs >= deadline) {
|
|
3006
|
+
const timeoutLine = {
|
|
3007
|
+
watch_status: "timeout",
|
|
3008
|
+
message: `Polling stopped after ${opts.timeoutMs / 1e3}s without reaching a terminal status.`,
|
|
3009
|
+
last_status: deps.lastStatusOf(record)
|
|
3010
|
+
};
|
|
3011
|
+
deps.writeLine(timeoutLine);
|
|
3012
|
+
return;
|
|
3013
|
+
}
|
|
3014
|
+
await deps.sleep(opts.intervalMs);
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
async function watchOrderStatus(apiClient, apiKey, orderId, opts) {
|
|
3018
|
+
const path = `/hotel/${encodeURIComponent(orderId)}/status`;
|
|
3019
|
+
await runHotelWatch(
|
|
3020
|
+
{
|
|
3021
|
+
fetchStatus: async () => {
|
|
3022
|
+
const result = await apiClient.get(path, {
|
|
3023
|
+
type: "api-key",
|
|
3024
|
+
key: apiKey
|
|
3025
|
+
});
|
|
3026
|
+
if (!result.success) {
|
|
3027
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
3028
|
+
}
|
|
3029
|
+
return result.data;
|
|
3030
|
+
},
|
|
3031
|
+
writeLine: ndjsonWriteLine,
|
|
3032
|
+
sleep: realSleep,
|
|
3033
|
+
now: () => Date.now(),
|
|
3034
|
+
isTerminal: isTerminalOrderStatus,
|
|
3035
|
+
lastStatusOf: lastOrderStatusOf
|
|
3036
|
+
},
|
|
3037
|
+
{
|
|
3038
|
+
intervalMs: opts.intervalSeconds * 1e3,
|
|
3039
|
+
timeoutMs: opts.timeoutSeconds * 1e3
|
|
3040
|
+
}
|
|
3041
|
+
);
|
|
3042
|
+
}
|
|
3043
|
+
async function watchCheckoutStatus(apiClient, apiKey, taskOrderCode, opts) {
|
|
3044
|
+
const path = `/hotel/checkout/${encodeURIComponent(taskOrderCode)}`;
|
|
3045
|
+
await runHotelWatch(
|
|
3046
|
+
{
|
|
3047
|
+
fetchStatus: async () => {
|
|
3048
|
+
const result = await apiClient.get(path, {
|
|
3049
|
+
type: "api-key",
|
|
3050
|
+
key: apiKey
|
|
3051
|
+
});
|
|
3052
|
+
if (!result.success) {
|
|
3053
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
3054
|
+
}
|
|
3055
|
+
return result.data;
|
|
3056
|
+
},
|
|
3057
|
+
writeLine: ndjsonWriteLine,
|
|
3058
|
+
sleep: realSleep,
|
|
3059
|
+
now: () => Date.now(),
|
|
3060
|
+
isTerminal: isTerminalRefundStatus,
|
|
3061
|
+
lastStatusOf: lastRefundStatusOf
|
|
3062
|
+
},
|
|
3063
|
+
{
|
|
3064
|
+
intervalMs: opts.intervalSeconds * 1e3,
|
|
3065
|
+
timeoutMs: opts.timeoutSeconds * 1e3
|
|
3066
|
+
}
|
|
3067
|
+
);
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
// src/hotel-redaug/pay-order.ts
|
|
3071
|
+
var DEFAULT_PAY_WATCH_INTERVAL_SECONDS = 5;
|
|
3072
|
+
var DEFAULT_PAY_WATCH_TIMEOUT_SECONDS = 300;
|
|
3073
|
+
function formatPayOrder(data) {
|
|
3074
|
+
const lines = [
|
|
3075
|
+
["Order ID", String(data.order_id ?? "-")],
|
|
3076
|
+
["Order status", String(data.status ?? "-")]
|
|
3077
|
+
];
|
|
3078
|
+
if (data.settlement_path) lines.push(["Settlement path", String(data.settlement_path)]);
|
|
3079
|
+
if (data.amount != null && data.currency) {
|
|
3080
|
+
lines.push(["Amount", `${data.amount} ${data.currency}`]);
|
|
3081
|
+
}
|
|
3082
|
+
return Formatter.keyValue(lines);
|
|
3083
|
+
}
|
|
3084
|
+
function isPaymentTerminal(record) {
|
|
3085
|
+
const status = record.status;
|
|
3086
|
+
return status === "PAID";
|
|
3087
|
+
}
|
|
3088
|
+
function registerHotelPayOrderCommand(parent, deps) {
|
|
3089
|
+
const cmd = parent.command("pay-order").description("Trigger supplier confirmation for a paid hotel order (upstream payOrder)").option("--api-key <key>", "API Key for authentication (X-Api-Key)").option("--order-id <id>", "Order ID to pay (from create-order response)").option(
|
|
3090
|
+
"--idempotency-key <key>",
|
|
3091
|
+
"Idempotency key forwarded verbatim as the Idempotency-Key header"
|
|
3092
|
+
).option("--watch", "Poll until PAID or timeout (NDJSON output)", false).option(
|
|
3093
|
+
"--watch-interval <seconds>",
|
|
3094
|
+
"Seconds between polls when --watch is set",
|
|
3095
|
+
String(DEFAULT_PAY_WATCH_INTERVAL_SECONDS)
|
|
3096
|
+
).option(
|
|
3097
|
+
"--watch-timeout <seconds>",
|
|
3098
|
+
"Max seconds to poll before giving up",
|
|
3099
|
+
String(DEFAULT_PAY_WATCH_TIMEOUT_SECONDS)
|
|
3100
|
+
);
|
|
3101
|
+
attachSchemaHelp(cmd, hotelPayOrderSchema);
|
|
3102
|
+
cmd.action(async () => {
|
|
3103
|
+
const opts = cmd.optsWithGlobals();
|
|
3104
|
+
const format = resolveFormat(opts.format);
|
|
3105
|
+
const isYes = Boolean(opts.yes);
|
|
3106
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
3107
|
+
message: "API Key:",
|
|
3108
|
+
type: "password"
|
|
3109
|
+
});
|
|
3110
|
+
const orderId = opts.orderId;
|
|
3111
|
+
if (orderId === void 0) {
|
|
3112
|
+
throw new CliError("PARAM_INVALID", "Missing required --order-id.");
|
|
3113
|
+
}
|
|
3114
|
+
const idempotencyKey = await resolveIdempotencyKey(opts.idempotencyKey, {
|
|
3115
|
+
yes: isYes,
|
|
3116
|
+
commandPath: "hotel-redaug pay-order"
|
|
3117
|
+
});
|
|
3118
|
+
const watchEnabled = Boolean(opts.watch);
|
|
3119
|
+
const watchInterval = resolveSeconds(opts.watchInterval, DEFAULT_PAY_WATCH_INTERVAL_SECONDS);
|
|
3120
|
+
const watchTimeout = resolveSeconds(opts.watchTimeout, DEFAULT_PAY_WATCH_TIMEOUT_SECONDS);
|
|
3121
|
+
const body = {};
|
|
3122
|
+
if (!isYes) {
|
|
3123
|
+
const confirmed = await confirm4({
|
|
3124
|
+
message: `Confirm hotel order ${orderId} with the supplier? (Payment was already settled in create-order; this triggers supplier confirmation.)`,
|
|
3125
|
+
default: true
|
|
3126
|
+
});
|
|
3127
|
+
if (!confirmed) {
|
|
3128
|
+
throw new CliError("CLIENT_ABORTED", "Confirmation aborted by user.");
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
const path = `/hotel/${encodeURIComponent(orderId)}/pay`;
|
|
3132
|
+
const headers = { "Idempotency-Key": idempotencyKey };
|
|
3133
|
+
const auth = { type: "api-key", key: apiKey };
|
|
3134
|
+
if (!watchEnabled) {
|
|
3135
|
+
const spinner = format === "json" ? null : createSpinner("Paying hotel order...");
|
|
3136
|
+
const result = await deps.apiClient.post(
|
|
3137
|
+
path,
|
|
3138
|
+
auth,
|
|
3139
|
+
body,
|
|
3140
|
+
headers
|
|
3141
|
+
);
|
|
3142
|
+
spinner?.stop();
|
|
3143
|
+
if (!result.success) {
|
|
3144
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
3145
|
+
}
|
|
3146
|
+
const data = result.data;
|
|
3147
|
+
const configManager = new ConfigManager();
|
|
3148
|
+
const commandResult = {
|
|
3149
|
+
data,
|
|
3150
|
+
text: () => formatPayOrder(data)
|
|
3151
|
+
};
|
|
3152
|
+
await renderWithContext(commandResult, { format }, configManager);
|
|
3153
|
+
} else {
|
|
3154
|
+
const deadline = Date.now() + watchTimeout * 1e3;
|
|
3155
|
+
let lastRecord = null;
|
|
3156
|
+
for (; ; ) {
|
|
3157
|
+
const result = await deps.apiClient.post(
|
|
3158
|
+
path,
|
|
3159
|
+
auth,
|
|
3160
|
+
body,
|
|
3161
|
+
headers
|
|
3162
|
+
);
|
|
3163
|
+
if (result.success) {
|
|
3164
|
+
const data = result.data;
|
|
3165
|
+
lastRecord = data;
|
|
3166
|
+
ndjsonWriteLine(data);
|
|
3167
|
+
if (isPaymentTerminal(data)) {
|
|
3168
|
+
return;
|
|
3169
|
+
}
|
|
3170
|
+
} else {
|
|
3171
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
3172
|
+
}
|
|
3173
|
+
if (Date.now() + watchInterval * 1e3 >= deadline) {
|
|
3174
|
+
const timeoutLine = {
|
|
3175
|
+
watch_status: "timeout",
|
|
3176
|
+
message: `Polling stopped after ${watchTimeout}s without reaching PAID status.`,
|
|
3177
|
+
last_status: lastRecord
|
|
3178
|
+
};
|
|
3179
|
+
ndjsonWriteLine(timeoutLine);
|
|
3180
|
+
return;
|
|
3181
|
+
}
|
|
3182
|
+
await realSleep(watchInterval * 1e3);
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
});
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
// src/hotel-redaug/get.ts
|
|
3189
|
+
function need8(value, flag) {
|
|
3190
|
+
if (value === void 0) {
|
|
3191
|
+
throw new CliError("PARAM_INVALID", `Missing required --${flag}.`);
|
|
3192
|
+
}
|
|
3193
|
+
return value;
|
|
3194
|
+
}
|
|
3195
|
+
function formatGetOrder2(data) {
|
|
3196
|
+
const lines = [
|
|
3197
|
+
["Order ID", String(data.order_id ?? "-")],
|
|
3198
|
+
["FC Order Code", String(data.fc_order_code ?? "-")],
|
|
3199
|
+
["Status", String(data.order_status ?? "-")]
|
|
3200
|
+
];
|
|
3201
|
+
if (data.order_status_code !== void 0) {
|
|
3202
|
+
lines.push(["Status code", String(data.order_status_code)]);
|
|
3203
|
+
}
|
|
3204
|
+
if (data.hotel_confirm_no !== void 0 && data.hotel_confirm_no !== null) {
|
|
3205
|
+
lines.push(["Hotel confirm no", String(data.hotel_confirm_no)]);
|
|
3206
|
+
}
|
|
3207
|
+
if (data.hotel_name) lines.push(["Hotel", String(data.hotel_name)]);
|
|
3208
|
+
if (data.room_name) lines.push(["Room", String(data.room_name)]);
|
|
3209
|
+
if (data.check_in) lines.push(["Check-in", String(data.check_in)]);
|
|
3210
|
+
if (data.check_out) lines.push(["Check-out", String(data.check_out)]);
|
|
3211
|
+
if (data.total_amount !== void 0 && data.total_amount !== null) {
|
|
3212
|
+
const currency = data.currency ? ` ${data.currency}` : "";
|
|
3213
|
+
lines.push(["Total amount", `${String(data.total_amount)}${currency}`]);
|
|
3214
|
+
}
|
|
3215
|
+
if (data.settlement_path) lines.push(["Settlement path", String(data.settlement_path)]);
|
|
3216
|
+
if (data.rooms && data.rooms.length > 0) {
|
|
3217
|
+
lines.push([
|
|
3218
|
+
"Rooms",
|
|
3219
|
+
data.rooms.map((r) => `${r.room_index}:${r.guest_name}`).join(", ")
|
|
3220
|
+
]);
|
|
3221
|
+
}
|
|
3222
|
+
if (data.room_num !== void 0 && data.room_num !== null) {
|
|
3223
|
+
lines.push(["Rooms booked", String(data.room_num)]);
|
|
3224
|
+
}
|
|
3225
|
+
if (data.guest?.name) lines.push(["Guest", String(data.guest.name)]);
|
|
3226
|
+
if (data.contact?.name) {
|
|
3227
|
+
const phone2 = data.contact.phone ? ` (+${data.contact.country_code ?? ""} ${data.contact.phone})` : "";
|
|
3228
|
+
lines.push(["Contact", `${data.contact.name}${phone2}`]);
|
|
3229
|
+
}
|
|
3230
|
+
if (data.arrive_time) lines.push(["Arrive time", String(data.arrive_time)]);
|
|
3231
|
+
if (data.special_requests) lines.push(["Special requests", String(data.special_requests)]);
|
|
3232
|
+
if (data.channel_state) lines.push(["Channel state", String(data.channel_state)]);
|
|
3233
|
+
if (data.created_at) lines.push(["Created at", String(data.created_at)]);
|
|
3234
|
+
if (data.updated_at) lines.push(["Updated at", String(data.updated_at)]);
|
|
3235
|
+
return Formatter.keyValue(lines);
|
|
3236
|
+
}
|
|
3237
|
+
function registerHotelGetCommand(parent, deps) {
|
|
3238
|
+
const cmd = parent.command("get").description("Retrieve a hotel order status by id (repeatable for polling)").option("--api-key <key>", "API Key for authentication (X-Api-Key)").option("--order-id <id>", "Our order reference (coOrderCode) returned by `hotel-redaug create-order`").option("--watch", "Poll until a terminal status, emitting one NDJSON line per update").option(
|
|
3239
|
+
"--watch-interval <seconds>",
|
|
3240
|
+
`Seconds between polls when --watch is set (default ${DEFAULT_ORDER_WATCH_INTERVAL_SECONDS})`
|
|
3241
|
+
).option(
|
|
3242
|
+
"--watch-timeout <seconds>",
|
|
3243
|
+
`Max seconds to poll before giving up (default ${DEFAULT_WATCH_TIMEOUT_SECONDS2})`
|
|
3244
|
+
);
|
|
3245
|
+
attachSchemaHelp(cmd, hotelGetSchema);
|
|
3246
|
+
cmd.action(async () => {
|
|
3247
|
+
const opts = cmd.optsWithGlobals();
|
|
3248
|
+
const format = resolveFormat(opts.format);
|
|
3249
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
3250
|
+
message: "API Key:",
|
|
3251
|
+
type: "password"
|
|
3252
|
+
});
|
|
3253
|
+
const orderId = need8(opts.orderId, "order-id");
|
|
3254
|
+
if (opts.watch) {
|
|
3255
|
+
await watchOrderStatus(deps.apiClient, apiKey, orderId, {
|
|
3256
|
+
intervalSeconds: resolveSeconds(
|
|
3257
|
+
opts.watchInterval,
|
|
3258
|
+
DEFAULT_ORDER_WATCH_INTERVAL_SECONDS
|
|
3259
|
+
),
|
|
3260
|
+
timeoutSeconds: resolveSeconds(
|
|
3261
|
+
opts.watchTimeout,
|
|
3262
|
+
DEFAULT_WATCH_TIMEOUT_SECONDS2
|
|
3263
|
+
)
|
|
3264
|
+
});
|
|
3265
|
+
return;
|
|
3266
|
+
}
|
|
3267
|
+
const spinner = format === "json" ? null : createSpinner("Fetching hotel order status...");
|
|
3268
|
+
const result = await deps.apiClient.get(
|
|
3269
|
+
`/hotel/${encodeURIComponent(orderId)}/status`,
|
|
3270
|
+
{ type: "api-key", key: apiKey }
|
|
3271
|
+
);
|
|
3272
|
+
spinner?.stop();
|
|
3273
|
+
if (!result.success) {
|
|
3274
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
3275
|
+
}
|
|
3276
|
+
const data = result.data;
|
|
3277
|
+
const configManager = new ConfigManager();
|
|
3278
|
+
const commandResult = {
|
|
3279
|
+
data,
|
|
3280
|
+
text: () => formatGetOrder2(data)
|
|
3281
|
+
};
|
|
3282
|
+
await renderWithContext(commandResult, { format }, configManager);
|
|
3283
|
+
});
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
// src/hotel-redaug/cancel.ts
|
|
3287
|
+
import { confirm as confirm5 } from "@inquirer/prompts";
|
|
3288
|
+
function need9(value, flag) {
|
|
3289
|
+
if (value === void 0) {
|
|
3290
|
+
throw new CliError("PARAM_INVALID", `Missing required --${flag}.`);
|
|
3291
|
+
}
|
|
3292
|
+
return value;
|
|
3293
|
+
}
|
|
3294
|
+
function formatCancel2(data) {
|
|
3295
|
+
const lines = [
|
|
3296
|
+
["Order ID", String(data.order_id ?? "-")],
|
|
3297
|
+
["Order status", String(data.order_status ?? "-")]
|
|
3298
|
+
];
|
|
3299
|
+
if (data.cancel_status !== void 0) {
|
|
3300
|
+
lines.push(["Cancel status", String(data.cancel_status)]);
|
|
3301
|
+
}
|
|
3302
|
+
if (data.cancellation) {
|
|
3303
|
+
const c = data.cancellation;
|
|
3304
|
+
const cur = c.currency ? ` ${c.currency}` : "";
|
|
3305
|
+
if (c.cancellation_fee !== void 0) {
|
|
3306
|
+
lines.push(["Cancellation fee", `${c.cancellation_fee}${cur}`]);
|
|
3307
|
+
}
|
|
3308
|
+
if (c.reversal_amount !== void 0) {
|
|
3309
|
+
lines.push(["Reversal amount", `${c.reversal_amount}${cur}`]);
|
|
3310
|
+
}
|
|
3311
|
+
} else if (data.cancellation_fee !== void 0 && data.cancellation_fee !== null) {
|
|
3312
|
+
lines.push(["Cancellation fee", String(data.cancellation_fee)]);
|
|
3313
|
+
}
|
|
3314
|
+
if (data.refund_amount !== void 0 && data.refund_amount !== null) {
|
|
3315
|
+
lines.push(["Refund amount", String(data.refund_amount)]);
|
|
3316
|
+
}
|
|
3317
|
+
const out = [Formatter.keyValue(lines)];
|
|
3318
|
+
out.push(
|
|
3319
|
+
"",
|
|
3320
|
+
Formatter.status(
|
|
3321
|
+
"info",
|
|
3322
|
+
`A successful response is acceptance only, NOT proof of cancellation \u2014 poll 'hotel-redaug get --order-id ${data.order_id ?? "<order_id>"}' until order_status=CANCELLED (code 4) to confirm.`
|
|
3323
|
+
)
|
|
3324
|
+
);
|
|
3325
|
+
return out.join("\n");
|
|
3326
|
+
}
|
|
3327
|
+
function registerHotelCancelCommand(parent, deps) {
|
|
3328
|
+
const cmd = parent.command("cancel").description("Cancel a whole hotel order within its policy (a cancellation fee may apply)").option("--api-key <key>", "API Key for authentication (X-Api-Key)").option("--order-id <id>", "Our order reference (coOrderCode) to cancel (book.response.order_id)").option("--fc-order-code <code>", "Supplier order reference (book.response.fc_order_code)").option("--reason <text>", "Cancellation reason").option(
|
|
3329
|
+
"--idempotency-key <key>",
|
|
3330
|
+
"Idempotency key forwarded verbatim as the Idempotency-Key header"
|
|
3331
|
+
);
|
|
3332
|
+
attachSchemaHelp(cmd, hotelCancelSchema);
|
|
3333
|
+
cmd.action(async () => {
|
|
3334
|
+
const opts = cmd.optsWithGlobals();
|
|
3335
|
+
const format = resolveFormat(opts.format);
|
|
3336
|
+
const isYes = Boolean(opts.yes);
|
|
3337
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
3338
|
+
message: "API Key:",
|
|
3339
|
+
type: "password"
|
|
3340
|
+
});
|
|
3341
|
+
const orderId = need9(opts.orderId, "order-id");
|
|
3342
|
+
const fcOrderCode = need9(opts.fcOrderCode, "fc-order-code");
|
|
3343
|
+
const body = { fc_order_code: fcOrderCode };
|
|
3344
|
+
if (opts.reason !== void 0) body.reason = opts.reason;
|
|
3345
|
+
if (!isYes) {
|
|
3346
|
+
const confirmed = await confirm5({
|
|
3347
|
+
message: `Cancel hotel order ${orderId}? A cancellation fee may apply.`,
|
|
3348
|
+
default: false
|
|
3349
|
+
});
|
|
3350
|
+
if (!confirmed) {
|
|
3351
|
+
throw new CliError("CLIENT_ABORTED", "Cancellation aborted by user.");
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
const idempotencyKey = await resolveIdempotencyKey(opts.idempotencyKey, {
|
|
3355
|
+
yes: isYes,
|
|
3356
|
+
commandPath: "hotel-redaug cancel"
|
|
3357
|
+
});
|
|
3358
|
+
const spinner = format === "json" ? null : createSpinner("Cancelling hotel order...");
|
|
3359
|
+
const result = await deps.apiClient.post(
|
|
3360
|
+
`/hotel/${encodeURIComponent(orderId)}/cancel`,
|
|
3361
|
+
{ type: "api-key", key: apiKey },
|
|
3362
|
+
body,
|
|
3363
|
+
{ "Idempotency-Key": idempotencyKey }
|
|
3364
|
+
);
|
|
3365
|
+
spinner?.stop();
|
|
3366
|
+
if (!result.success) {
|
|
3367
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
3368
|
+
}
|
|
3369
|
+
const data = result.data;
|
|
3370
|
+
const configManager = new ConfigManager();
|
|
3371
|
+
const commandResult = {
|
|
3372
|
+
data,
|
|
3373
|
+
text: () => formatCancel2(data)
|
|
3374
|
+
};
|
|
3375
|
+
await renderWithContext(commandResult, { format }, configManager);
|
|
3376
|
+
});
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
// src/hotel-redaug/checkout.ts
|
|
3380
|
+
import { confirm as confirm6 } from "@inquirer/prompts";
|
|
3381
|
+
function need10(value, flag) {
|
|
3382
|
+
if (value === void 0) {
|
|
3383
|
+
throw new CliError("PARAM_INVALID", `Missing required --${flag}.`);
|
|
3384
|
+
}
|
|
3385
|
+
return value;
|
|
3386
|
+
}
|
|
3387
|
+
function num6(value, flag) {
|
|
3388
|
+
const n = Number(need10(value, flag));
|
|
3389
|
+
if (!Number.isFinite(n)) {
|
|
3390
|
+
throw new CliError("PARAM_INVALID", `--${flag} must be a number.`);
|
|
3391
|
+
}
|
|
3392
|
+
return n;
|
|
3393
|
+
}
|
|
3394
|
+
function parseCheckoutRooms(raw) {
|
|
3395
|
+
const INVALID = "--checkout-rooms must be a JSON array of {room_index (string), guest_name (string), cancel_check_in_date (string)}.";
|
|
3396
|
+
let parsed;
|
|
3397
|
+
try {
|
|
3398
|
+
parsed = JSON.parse(raw);
|
|
3399
|
+
} catch {
|
|
3400
|
+
throw new CliError("PARAM_INVALID", INVALID);
|
|
3401
|
+
}
|
|
3402
|
+
if (!Array.isArray(parsed)) {
|
|
3403
|
+
throw new CliError("PARAM_INVALID", INVALID);
|
|
3404
|
+
}
|
|
3405
|
+
for (const el of parsed) {
|
|
3406
|
+
if (typeof el !== "object" || el === null || Array.isArray(el)) {
|
|
3407
|
+
throw new CliError("PARAM_INVALID", INVALID);
|
|
3408
|
+
}
|
|
3409
|
+
const room = el;
|
|
3410
|
+
if (typeof room.room_index !== "string" || typeof room.guest_name !== "string" || typeof room.cancel_check_in_date !== "string") {
|
|
3411
|
+
throw new CliError("PARAM_INVALID", INVALID);
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
return parsed;
|
|
3415
|
+
}
|
|
3416
|
+
function formatCheckout(data) {
|
|
3417
|
+
const lines = [
|
|
3418
|
+
["Order ID", String(data.order_id ?? "-")],
|
|
3419
|
+
["Task order code", String(data.task_order_code ?? "-")]
|
|
3420
|
+
];
|
|
3421
|
+
if (data.apply_status !== void 0 && data.apply_status !== null) {
|
|
3422
|
+
lines.push(["Apply status", String(data.apply_status)]);
|
|
3423
|
+
}
|
|
3424
|
+
if (data.checkout_status !== void 0) {
|
|
3425
|
+
lines.push(["Checkout status", String(data.checkout_status)]);
|
|
3426
|
+
}
|
|
3427
|
+
const out = [Formatter.keyValue(lines)];
|
|
3428
|
+
out.push(
|
|
3429
|
+
"",
|
|
3430
|
+
Formatter.status(
|
|
3431
|
+
"info",
|
|
3432
|
+
`Check-out is ASYNC \u2014 this response is acceptance only, NOT proof of refund. Poll 'hotel-redaug get-checkout --task-order-code ${data.task_order_code ?? "<task_order_code>"}' until refund_status is approved/rejected/refunded.`
|
|
3433
|
+
)
|
|
3434
|
+
);
|
|
3435
|
+
return out.join("\n");
|
|
3436
|
+
}
|
|
3437
|
+
function registerHotelCheckoutCommand(parent, deps) {
|
|
3438
|
+
const cmd = parent.command("checkout").description("Request a partial check-out or an out-of-policy cancellation (async; the property must approve)").option("--api-key <key>", "API Key for authentication (X-Api-Key)").option("--order-id <id>", "Our order reference (coOrderCode) for the URL path (book.response.order_id)").option("--fc-order-code <code>", "Supplier order reference of the booking to change (book.response.fc_order_code)").option("--reason <text>", "Why the partial check-out / out-of-policy cancellation is requested").option(
|
|
3439
|
+
"--checkout-rooms <json>",
|
|
3440
|
+
"Rooms to check out of as a JSON array of {room_index, guest_name, cancel_check_in_date} (room_index/guest_name copied verbatim from book.response.rooms[])"
|
|
3441
|
+
).option("--refund-type <type>", "Requested refund handling (upstream refund-type code)", "1").option(
|
|
3442
|
+
"--idempotency-key <key>",
|
|
3443
|
+
"Idempotency key forwarded verbatim as the Idempotency-Key header"
|
|
3444
|
+
);
|
|
3445
|
+
attachSchemaHelp(cmd, hotelCheckoutSchema);
|
|
3446
|
+
cmd.action(async () => {
|
|
3447
|
+
const opts = cmd.optsWithGlobals();
|
|
3448
|
+
const format = resolveFormat(opts.format);
|
|
3449
|
+
const isYes = Boolean(opts.yes);
|
|
3450
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
3451
|
+
message: "API Key:",
|
|
3452
|
+
type: "password"
|
|
3453
|
+
});
|
|
3454
|
+
const orderId = need10(opts.orderId, "order-id");
|
|
3455
|
+
const fcOrderCode = need10(opts.fcOrderCode, "fc-order-code");
|
|
3456
|
+
const reason = need10(opts.reason, "reason");
|
|
3457
|
+
const checkoutRooms = parseCheckoutRooms(need10(opts.checkoutRooms, "checkout-rooms"));
|
|
3458
|
+
const refundType = num6(opts.refundType, "refund-type");
|
|
3459
|
+
const body = {
|
|
3460
|
+
fc_order_code: fcOrderCode,
|
|
3461
|
+
reason,
|
|
3462
|
+
refund_type: refundType,
|
|
3463
|
+
checkout_rooms: checkoutRooms
|
|
3464
|
+
};
|
|
3465
|
+
if (!isYes) {
|
|
3466
|
+
const confirmed = await confirm6({
|
|
3467
|
+
message: `Request check-out / out-of-policy cancellation for order ${orderId} (${checkoutRooms.length} room(s))? The property must approve and charges may apply.`,
|
|
3468
|
+
default: false
|
|
3469
|
+
});
|
|
3470
|
+
if (!confirmed) {
|
|
3471
|
+
throw new CliError("CLIENT_ABORTED", "Check-out application aborted by user.");
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
const idempotencyKey = await resolveIdempotencyKey(opts.idempotencyKey, {
|
|
3475
|
+
yes: isYes,
|
|
3476
|
+
commandPath: "hotel-redaug checkout"
|
|
3477
|
+
});
|
|
3478
|
+
const spinner = format === "json" ? null : createSpinner("Submitting check-out application...");
|
|
3479
|
+
const result = await deps.apiClient.post(
|
|
3480
|
+
`/hotel/${encodeURIComponent(orderId)}/checkout`,
|
|
3481
|
+
{ type: "api-key", key: apiKey },
|
|
3482
|
+
body,
|
|
3483
|
+
{ "Idempotency-Key": idempotencyKey }
|
|
3484
|
+
);
|
|
3485
|
+
spinner?.stop();
|
|
3486
|
+
if (!result.success) {
|
|
3487
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
3488
|
+
}
|
|
3489
|
+
const data = result.data;
|
|
3490
|
+
const configManager = new ConfigManager();
|
|
3491
|
+
const commandResult = {
|
|
3492
|
+
data,
|
|
3493
|
+
text: () => formatCheckout(data)
|
|
3494
|
+
};
|
|
3495
|
+
await renderWithContext(commandResult, { format }, configManager);
|
|
3496
|
+
});
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3499
|
+
// src/hotel-redaug/get-checkout.ts
|
|
3500
|
+
function need11(value, flag) {
|
|
3501
|
+
if (value === void 0) {
|
|
3502
|
+
throw new CliError("PARAM_INVALID", `Missing required --${flag}.`);
|
|
3503
|
+
}
|
|
3504
|
+
return value;
|
|
3505
|
+
}
|
|
3506
|
+
function formatGetCheckout(data) {
|
|
3507
|
+
const lines = [
|
|
3508
|
+
["Task order code", String(data.task_order_code ?? "-")],
|
|
3509
|
+
["Refund status", String(data.refund_status ?? "-")]
|
|
3510
|
+
];
|
|
3511
|
+
if (data.refund !== void 0 && data.refund !== null) {
|
|
3512
|
+
lines.push(["Refund amount", String(data.refund.amount)]);
|
|
3513
|
+
lines.push(["Refund currency", String(data.refund.currency)]);
|
|
3514
|
+
}
|
|
3515
|
+
return Formatter.keyValue(lines);
|
|
3516
|
+
}
|
|
3517
|
+
function registerHotelGetCheckoutCommand(parent, deps) {
|
|
3518
|
+
const cmd = parent.command("get-checkout").description("Retrieve a check-out application status and refund outcome (repeatable for polling)").option("--api-key <key>", "API Key for authentication (X-Api-Key)").option(
|
|
3519
|
+
"--task-order-code <code>",
|
|
3520
|
+
"Check-out application id returned by `hotel-redaug checkout`"
|
|
3521
|
+
).option("--watch", "Poll until a terminal status, emitting one NDJSON line per update").option(
|
|
3522
|
+
"--watch-interval <seconds>",
|
|
3523
|
+
`Seconds between polls when --watch is set (default ${DEFAULT_CHECKOUT_WATCH_INTERVAL_SECONDS})`
|
|
3524
|
+
).option(
|
|
3525
|
+
"--watch-timeout <seconds>",
|
|
3526
|
+
`Max seconds to poll before giving up (default ${DEFAULT_WATCH_TIMEOUT_SECONDS2})`
|
|
3527
|
+
);
|
|
3528
|
+
attachSchemaHelp(cmd, hotelGetCheckoutSchema);
|
|
3529
|
+
cmd.action(async () => {
|
|
3530
|
+
const opts = cmd.optsWithGlobals();
|
|
3531
|
+
const format = resolveFormat(opts.format);
|
|
3532
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
3533
|
+
message: "API Key:",
|
|
3534
|
+
type: "password"
|
|
3535
|
+
});
|
|
3536
|
+
const taskOrderCode = need11(opts.taskOrderCode, "task-order-code");
|
|
3537
|
+
if (opts.watch) {
|
|
3538
|
+
await watchCheckoutStatus(deps.apiClient, apiKey, taskOrderCode, {
|
|
3539
|
+
intervalSeconds: resolveSeconds(
|
|
3540
|
+
opts.watchInterval,
|
|
3541
|
+
DEFAULT_CHECKOUT_WATCH_INTERVAL_SECONDS
|
|
3542
|
+
),
|
|
3543
|
+
timeoutSeconds: resolveSeconds(
|
|
3544
|
+
opts.watchTimeout,
|
|
3545
|
+
DEFAULT_WATCH_TIMEOUT_SECONDS2
|
|
3546
|
+
)
|
|
3547
|
+
});
|
|
3548
|
+
return;
|
|
3549
|
+
}
|
|
3550
|
+
const spinner = format === "json" ? null : createSpinner("Fetching check-out application status...");
|
|
3551
|
+
const result = await deps.apiClient.get(
|
|
3552
|
+
`/hotel/checkout/${encodeURIComponent(taskOrderCode)}`,
|
|
3553
|
+
{ type: "api-key", key: apiKey }
|
|
3554
|
+
);
|
|
3555
|
+
spinner?.stop();
|
|
3556
|
+
if (!result.success) {
|
|
3557
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
3558
|
+
}
|
|
3559
|
+
const data = result.data;
|
|
3560
|
+
const configManager = new ConfigManager();
|
|
3561
|
+
const commandResult = {
|
|
3562
|
+
data,
|
|
3563
|
+
text: () => formatGetCheckout(data)
|
|
3564
|
+
};
|
|
3565
|
+
await renderWithContext(commandResult, { format }, configManager);
|
|
3566
|
+
});
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
// src/hotel-redaug/list-orders.ts
|
|
3570
|
+
var DEFAULT_PAGE2 = "1";
|
|
3571
|
+
var DEFAULT_PAGE_SIZE2 = "20";
|
|
3572
|
+
function positiveInt3(value, flag) {
|
|
3573
|
+
const n = Number(value);
|
|
3574
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
|
|
3575
|
+
throw new CliError("PARAM_INVALID", `--${flag} must be a positive integer.`);
|
|
3576
|
+
}
|
|
3577
|
+
return String(n);
|
|
3578
|
+
}
|
|
3579
|
+
function formatOrders2(data) {
|
|
3580
|
+
const orders = data.orders ?? [];
|
|
3581
|
+
if (orders.length === 0) {
|
|
3582
|
+
return Formatter.status("info", "No hotel orders found");
|
|
3583
|
+
}
|
|
3584
|
+
const headers = ["Order ID", "FC Order Code", "Status", "Check-in", "Check-out", "Price", "Payment"];
|
|
3585
|
+
const rows = orders.map((o) => [
|
|
3586
|
+
String(o.order_id ?? "-"),
|
|
3587
|
+
String(o.fc_order_code ?? "-"),
|
|
3588
|
+
String(o.status ?? "-"),
|
|
3589
|
+
String(o.check_in ?? "-"),
|
|
3590
|
+
String(o.check_out ?? "-"),
|
|
3591
|
+
o.price_amount != null ? `${o.price_amount} ${o.price_currency ?? ""}`.trim() : "-",
|
|
3592
|
+
String(o.payment_status ?? "-")
|
|
3593
|
+
]);
|
|
3594
|
+
const summary = [
|
|
3595
|
+
["Total", String(data.total ?? "-")],
|
|
3596
|
+
["Page", String(data.page ?? "-")],
|
|
3597
|
+
["Page size", String(data.page_size ?? "-")]
|
|
3598
|
+
];
|
|
3599
|
+
return `${Formatter.table(headers, rows)}
|
|
3600
|
+
${Formatter.keyValue(summary)}`;
|
|
3601
|
+
}
|
|
3602
|
+
function registerHotelListOrdersCommand(parent, deps) {
|
|
3603
|
+
const cmd = parent.command("list-orders").description("List the developer's hotel orders with optional status filtering and pagination").option("--api-key <key>", "API Key for authentication (X-Api-Key)").option("--status <status>", "Filter by order status (omitted entirely when absent \u2192 all statuses)").option("--page <page>", "Page number", DEFAULT_PAGE2).option("--page-size <size>", "Page size", DEFAULT_PAGE_SIZE2);
|
|
3604
|
+
attachSchemaHelp(cmd, hotelListOrdersSchema);
|
|
3605
|
+
cmd.action(async () => {
|
|
3606
|
+
const opts = cmd.optsWithGlobals();
|
|
3607
|
+
const format = resolveFormat(opts.format);
|
|
3608
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
3609
|
+
message: "API Key:",
|
|
3610
|
+
type: "password"
|
|
3611
|
+
});
|
|
3612
|
+
const params = {
|
|
3613
|
+
page: positiveInt3(opts.page ?? DEFAULT_PAGE2, "page"),
|
|
3614
|
+
page_size: positiveInt3(opts.pageSize ?? DEFAULT_PAGE_SIZE2, "page-size")
|
|
3615
|
+
};
|
|
3616
|
+
if (opts.status !== void 0) params.status = opts.status;
|
|
3617
|
+
const spinner = format === "json" ? null : createSpinner("Fetching orders...");
|
|
3618
|
+
const result = await deps.apiClient.get(
|
|
3619
|
+
"/hotel/orders",
|
|
3620
|
+
{ type: "api-key", key: apiKey },
|
|
3621
|
+
params
|
|
3622
|
+
);
|
|
3623
|
+
spinner?.stop();
|
|
3624
|
+
if (!result.success) {
|
|
3625
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
3626
|
+
}
|
|
3627
|
+
const data = result.data;
|
|
3628
|
+
const configManager = new ConfigManager();
|
|
3629
|
+
const commandResult = {
|
|
3630
|
+
data,
|
|
3631
|
+
text: () => formatOrders2(data)
|
|
3632
|
+
};
|
|
3633
|
+
await renderWithContext(commandResult, { format }, configManager);
|
|
3634
|
+
});
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
// src/hotel-redaug/find-destination.ts
|
|
3638
|
+
function need12(value, flag) {
|
|
3639
|
+
if (value === void 0 || value.trim() === "") {
|
|
3640
|
+
throw new CliError("PARAM_INVALID", `Missing required --${flag}.`);
|
|
3641
|
+
}
|
|
3642
|
+
return value;
|
|
3643
|
+
}
|
|
3644
|
+
function formatFindDestination(data) {
|
|
3645
|
+
const destinations = data.destinations ?? [];
|
|
3646
|
+
if (destinations.length === 0) {
|
|
3647
|
+
return Formatter.status("info", "No destinations found for this keyword");
|
|
3648
|
+
}
|
|
3649
|
+
const headers = ["Destination ID", "Type", "Name", "City Name", "City Code", "Country", "Lat", "Lng"];
|
|
3650
|
+
const rows = destinations.map((d) => [
|
|
3651
|
+
String(d.destination_id ?? "-"),
|
|
3652
|
+
String(d.type ?? "-"),
|
|
3653
|
+
String(d.name ?? "-"),
|
|
3654
|
+
String(d.city_name ?? "-"),
|
|
3655
|
+
String(d.city_code ?? "-"),
|
|
3656
|
+
String(d.country_name ?? "-"),
|
|
3657
|
+
String(d.lat ?? "-"),
|
|
3658
|
+
String(d.lng ?? "-")
|
|
3659
|
+
]);
|
|
3660
|
+
return Formatter.table(headers, rows);
|
|
3661
|
+
}
|
|
3662
|
+
function registerHotelFindDestinationCommand(parent, deps) {
|
|
3663
|
+
const cmd = parent.command("find-destination").description("Search for a hotel destination by keyword (city, landmark, airport, etc.)").option("--api-key <key>", "API Key for authentication (X-Api-Key)").option("--keyword <text>", "Destination search keyword (required)").option("--data-type <type>", "Optional data type filter");
|
|
3664
|
+
attachSchemaHelp(cmd, hotelFindDestinationSchema);
|
|
3665
|
+
cmd.action(async () => {
|
|
3666
|
+
const opts = cmd.optsWithGlobals();
|
|
3667
|
+
const format = resolveFormat(opts.format);
|
|
3668
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
3669
|
+
message: "API Key:",
|
|
3670
|
+
type: "password"
|
|
3671
|
+
});
|
|
3672
|
+
const keyword = need12(opts.keyword, "keyword");
|
|
3673
|
+
const body = { keyword };
|
|
3674
|
+
if (opts.dataType !== void 0) {
|
|
3675
|
+
body.data_type = opts.dataType;
|
|
3676
|
+
}
|
|
3677
|
+
const result = await deps.apiClient.post(
|
|
3678
|
+
"/hotel/find-destination",
|
|
3679
|
+
{ type: "api-key", key: apiKey },
|
|
3680
|
+
body
|
|
3681
|
+
);
|
|
3682
|
+
if (!result.success) {
|
|
3683
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
3684
|
+
}
|
|
3685
|
+
const data = result.data;
|
|
3686
|
+
const configManager = new ConfigManager();
|
|
3687
|
+
const commandResult = {
|
|
3688
|
+
data,
|
|
3689
|
+
text: () => formatFindDestination(data)
|
|
3690
|
+
};
|
|
3691
|
+
await renderWithContext(commandResult, { format }, configManager);
|
|
3692
|
+
});
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
// src/hotel-redaug/hotel-filters.ts
|
|
3696
|
+
function need13(value, flag) {
|
|
3697
|
+
if (value === void 0 || value.trim() === "") {
|
|
3698
|
+
throw new CliError("PARAM_INVALID", `Missing required --${flag}.`);
|
|
3699
|
+
}
|
|
3700
|
+
return value;
|
|
3701
|
+
}
|
|
3702
|
+
function num7(value, flag) {
|
|
3703
|
+
const n = Number(need13(value, flag));
|
|
3704
|
+
if (!Number.isFinite(n)) {
|
|
3705
|
+
throw new CliError("PARAM_INVALID", `--${flag} must be a number.`);
|
|
3706
|
+
}
|
|
3707
|
+
return n;
|
|
3708
|
+
}
|
|
3709
|
+
function coord2(value, flag, min, max) {
|
|
3710
|
+
const n = num7(value, flag);
|
|
3711
|
+
if (n < min || n > max) {
|
|
3712
|
+
throw new CliError("PARAM_INVALID", `--${flag} must be between ${min} and ${max}.`);
|
|
3713
|
+
}
|
|
3714
|
+
return n;
|
|
3715
|
+
}
|
|
3716
|
+
function renderGroup(title, items, isFacility = false) {
|
|
3717
|
+
if (items.length === 0) return "";
|
|
3718
|
+
const headers = isFacility ? ["Code", "Name", "Count", "Type"] : ["Code", "Name", "Count"];
|
|
3719
|
+
const rows = items.map((item) => {
|
|
3720
|
+
const base = [
|
|
3721
|
+
String(item.code ?? "-"),
|
|
3722
|
+
String(item.name ?? "-"),
|
|
3723
|
+
String(item.count ?? "-")
|
|
3724
|
+
];
|
|
3725
|
+
if (isFacility) {
|
|
3726
|
+
base.push(String(item.type ?? "-"));
|
|
3727
|
+
}
|
|
3728
|
+
return base;
|
|
3729
|
+
});
|
|
3730
|
+
return `
|
|
3731
|
+
[${title}]
|
|
3732
|
+
${Formatter.table(headers, rows)}`;
|
|
3733
|
+
}
|
|
3734
|
+
function formatFilters(data) {
|
|
3735
|
+
const sections = [
|
|
3736
|
+
renderGroup("Stars (\u2192 search --star)", data.stars ?? []),
|
|
3737
|
+
renderGroup("Brands (\u2192 search --hotel-brand-codes)", data.brands ?? []),
|
|
3738
|
+
renderGroup("Groups", data.groups ?? []),
|
|
3739
|
+
renderGroup("Labels (\u2192 search --hotel-label-ids)", data.labels ?? []),
|
|
3740
|
+
renderGroup("Sub-Categories (\u2192 search --hotel-sub-category-ids)", data.sub_categories ?? []),
|
|
3741
|
+
renderGroup("Hotel Facilities (\u2192 search --hotel-facility-codes)", data.hotel_facilities ?? [], true),
|
|
3742
|
+
renderGroup("Room Facilities (\u2192 search --room-facility-codes)", data.room_facilities ?? [], true)
|
|
3743
|
+
].filter(Boolean);
|
|
3744
|
+
if (sections.length === 0) {
|
|
3745
|
+
return Formatter.status("info", "No filter options available for this location");
|
|
3746
|
+
}
|
|
3747
|
+
return sections.join("\n");
|
|
3748
|
+
}
|
|
3749
|
+
function registerHotelFiltersCommand(parent, deps) {
|
|
3750
|
+
const cmd = parent.command("hotel-filters").description("Get available filter options for hotel search at a given location (codes map to search filters)").option("--api-key <key>", "API Key for authentication (X-Api-Key)").option("--destination-id <id>", "Destination ID (alternative to --lat/--lng)").option("--lat <lat>", "Latitude (-90 to 90, requires --lng)").option("--lng <lng>", "Longitude (-180 to 180, requires --lat)").option("--distance <km>", "Search radius in kilometers", "20");
|
|
3751
|
+
attachSchemaHelp(cmd, hotelFiltersSchema);
|
|
3752
|
+
cmd.action(async () => {
|
|
3753
|
+
const opts = cmd.optsWithGlobals();
|
|
3754
|
+
const format = resolveFormat(opts.format);
|
|
3755
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
3756
|
+
message: "API Key:",
|
|
3757
|
+
type: "password"
|
|
3758
|
+
});
|
|
3759
|
+
const hasDestination = opts.destinationId !== void 0;
|
|
3760
|
+
const hasLat = opts.lat !== void 0;
|
|
3761
|
+
const hasLng = opts.lng !== void 0;
|
|
3762
|
+
const hasCoordBranch = hasLat || hasLng;
|
|
3763
|
+
if (hasDestination && hasCoordBranch) {
|
|
3764
|
+
throw new CliError(
|
|
3765
|
+
"PARAM_INVALID",
|
|
3766
|
+
"Supply either --destination-id OR --lat/--lng, not both."
|
|
3767
|
+
);
|
|
3768
|
+
}
|
|
3769
|
+
if (!hasDestination && !hasCoordBranch) {
|
|
3770
|
+
throw new CliError(
|
|
3771
|
+
"PARAM_INVALID",
|
|
3772
|
+
"Supply either --destination-id OR --lat/--lng (one location branch is required)."
|
|
3773
|
+
);
|
|
3774
|
+
}
|
|
3775
|
+
if (hasCoordBranch && !(hasLat && hasLng)) {
|
|
3776
|
+
throw new CliError(
|
|
3777
|
+
"PARAM_INVALID",
|
|
3778
|
+
"--lat and --lng must both be provided together."
|
|
3779
|
+
);
|
|
3780
|
+
}
|
|
3781
|
+
const body = {};
|
|
3782
|
+
if (hasDestination) {
|
|
3783
|
+
body.destination_id = need13(opts.destinationId, "destination-id");
|
|
3784
|
+
} else {
|
|
3785
|
+
body.lat = coord2(opts.lat, "lat", -90, 90);
|
|
3786
|
+
body.lng = coord2(opts.lng, "lng", -180, 180);
|
|
3787
|
+
}
|
|
3788
|
+
body.distance = num7(opts.distance, "distance");
|
|
3789
|
+
const result = await deps.apiClient.post(
|
|
3790
|
+
"/hotel/filters",
|
|
3791
|
+
{ type: "api-key", key: apiKey },
|
|
3792
|
+
body
|
|
3793
|
+
);
|
|
3794
|
+
if (!result.success) {
|
|
3795
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
3796
|
+
}
|
|
3797
|
+
const data = result.data;
|
|
3798
|
+
const configManager = new ConfigManager();
|
|
3799
|
+
const commandResult = {
|
|
3800
|
+
data,
|
|
3801
|
+
text: () => formatFilters(data)
|
|
3802
|
+
};
|
|
3803
|
+
await renderWithContext(commandResult, { format }, configManager);
|
|
3804
|
+
});
|
|
3805
|
+
}
|
|
3806
|
+
|
|
3807
|
+
// src/hotel-redaug/list-cities.ts
|
|
3808
|
+
function need14(value, flag) {
|
|
3809
|
+
if (value === void 0 || value.trim() === "") {
|
|
3810
|
+
throw new CliError("PARAM_INVALID", `Missing required --${flag}.`);
|
|
3811
|
+
}
|
|
3812
|
+
return value;
|
|
3813
|
+
}
|
|
3814
|
+
function formatListCities(data) {
|
|
3815
|
+
const cities = data.cities ?? [];
|
|
3816
|
+
if (cities.length === 0) {
|
|
3817
|
+
return Formatter.status("info", "No cities found for this country");
|
|
3818
|
+
}
|
|
3819
|
+
const headers = ["City Code", "City Name", "Destination ID", "Lat", "Lng", "Country", "Time Zone"];
|
|
3820
|
+
const rows = cities.map((c) => [
|
|
3821
|
+
String(c.city_code ?? "-"),
|
|
3822
|
+
String(c.city_name ?? "-"),
|
|
3823
|
+
String(c.destination_id ?? "-"),
|
|
3824
|
+
String(c.lat ?? "-"),
|
|
3825
|
+
String(c.lng ?? "-"),
|
|
3826
|
+
String(c.country_name ?? "-"),
|
|
3827
|
+
String(c.time_zone ?? "-")
|
|
3828
|
+
]);
|
|
3829
|
+
return Formatter.table(headers, rows);
|
|
3830
|
+
}
|
|
3831
|
+
function registerHotelListCitiesCommand(parent, deps) {
|
|
3832
|
+
const cmd = parent.command("list-cities").description("List cities available for hotel booking in a given country").option("--api-key <key>", "API Key for authentication (X-Api-Key)").option("--country <code>", "ISO country code (required)");
|
|
3833
|
+
attachSchemaHelp(cmd, hotelListCitiesSchema);
|
|
3834
|
+
cmd.action(async () => {
|
|
3835
|
+
const opts = cmd.optsWithGlobals();
|
|
3836
|
+
const format = resolveFormat(opts.format);
|
|
3837
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
3838
|
+
message: "API Key:",
|
|
3839
|
+
type: "password"
|
|
3840
|
+
});
|
|
3841
|
+
const country = need14(opts.country, "country");
|
|
3842
|
+
const body = { country_code: country };
|
|
3843
|
+
const result = await deps.apiClient.post(
|
|
3844
|
+
"/hotel/cities",
|
|
3845
|
+
{ type: "api-key", key: apiKey },
|
|
3846
|
+
body
|
|
3847
|
+
);
|
|
3848
|
+
if (!result.success) {
|
|
3849
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
3850
|
+
}
|
|
3851
|
+
const data = result.data;
|
|
3852
|
+
const configManager = new ConfigManager();
|
|
3853
|
+
const commandResult = {
|
|
3854
|
+
data,
|
|
3855
|
+
text: () => formatListCities(data)
|
|
3856
|
+
};
|
|
3857
|
+
await renderWithContext(commandResult, { format }, configManager);
|
|
3858
|
+
});
|
|
3859
|
+
}
|
|
3860
|
+
|
|
3861
|
+
// src/hotel-redaug/hotel-detail.ts
|
|
3862
|
+
function need15(value, flag) {
|
|
3863
|
+
if (value === void 0 || value.trim() === "") {
|
|
3864
|
+
throw new CliError("PARAM_INVALID", `Missing required --${flag}.`);
|
|
3865
|
+
}
|
|
3866
|
+
return value;
|
|
3867
|
+
}
|
|
3868
|
+
function formatHotelDetail(data) {
|
|
3869
|
+
const lines = [];
|
|
3870
|
+
const kvPairs = [
|
|
3871
|
+
["Hotel ID", String(data.hotel_id ?? "-")],
|
|
3872
|
+
["Hotel Name", String(data.hotel_name ?? "-")],
|
|
3873
|
+
["English Name", String(data.hotel_eng_name ?? "-")],
|
|
3874
|
+
["Star", String(data.star ?? "-")],
|
|
3875
|
+
["Address", String(data.address ?? "-")],
|
|
3876
|
+
["Intro", String(data.intro ?? "-")],
|
|
3877
|
+
["Telephone", String(data.telephone ?? "-")],
|
|
3878
|
+
["Country", String(data.country_name ?? "-")],
|
|
3879
|
+
["Province", String(data.province_name ?? "-")],
|
|
3880
|
+
["City", String(data.city_name ?? "-")],
|
|
3881
|
+
["District", String(data.district_name ?? "-")],
|
|
3882
|
+
["Business Area", String(data.business_name ?? "-")],
|
|
3883
|
+
["Latitude", String(data.lat ?? "-")],
|
|
3884
|
+
["Longitude", String(data.lng ?? "-")],
|
|
3885
|
+
["Check-in Time", String(data.check_in_time ?? "-")],
|
|
3886
|
+
["Check-out Time", String(data.check_out_time ?? "-")],
|
|
3887
|
+
["Room Count", String(data.room_num ?? "-")]
|
|
3888
|
+
];
|
|
3889
|
+
lines.push(Formatter.keyValue(kvPairs));
|
|
3890
|
+
const facilities = data.facilities ?? [];
|
|
3891
|
+
if (facilities.length > 0) {
|
|
3892
|
+
lines.push("");
|
|
3893
|
+
lines.push("Facilities:");
|
|
3894
|
+
const facHeaders = ["Name", "Type"];
|
|
3895
|
+
const facRows = facilities.map((f) => [
|
|
3896
|
+
String(f.name ?? "-"),
|
|
3897
|
+
String(f.type ?? "-")
|
|
3898
|
+
]);
|
|
3899
|
+
lines.push(Formatter.table(facHeaders, facRows));
|
|
3900
|
+
} else {
|
|
3901
|
+
lines.push("");
|
|
3902
|
+
lines.push(Formatter.status("info", "No facilities listed"));
|
|
3903
|
+
}
|
|
3904
|
+
const images = data.images ?? [];
|
|
3905
|
+
if (images.length > 0) {
|
|
3906
|
+
lines.push("");
|
|
3907
|
+
lines.push("Images:");
|
|
3908
|
+
const imgHeaders = ["URL", "Is Main"];
|
|
3909
|
+
const imgRows = images.map((img) => [
|
|
3910
|
+
String(img.url ?? "-"),
|
|
3911
|
+
String(img.is_main ?? false)
|
|
3912
|
+
]);
|
|
3913
|
+
lines.push(Formatter.table(imgHeaders, imgRows));
|
|
3914
|
+
} else {
|
|
3915
|
+
lines.push("");
|
|
3916
|
+
lines.push(Formatter.status("info", "No images available"));
|
|
3917
|
+
}
|
|
3918
|
+
const rooms = data.rooms ?? [];
|
|
3919
|
+
if (rooms.length > 0) {
|
|
3920
|
+
lines.push("");
|
|
3921
|
+
lines.push("Rooms:");
|
|
3922
|
+
const roomHeaders = ["Room ID", "Room Name", "Area", "Floor", "Max Person", "Beds", "Images"];
|
|
3923
|
+
const roomRows = rooms.map((r) => {
|
|
3924
|
+
const bedsStr = (r.beds ?? []).map((b) => `${b.name ?? "-"}${b.num ? ` x${b.num}` : ""}`).join(", ") || "-";
|
|
3925
|
+
return [
|
|
3926
|
+
String(r.room_id ?? "-"),
|
|
3927
|
+
String(r.room_name ?? "-"),
|
|
3928
|
+
String(r.area_sqm ?? "-"),
|
|
3929
|
+
String(r.floor ?? "-"),
|
|
3930
|
+
String(r.max_person ?? "-"),
|
|
3931
|
+
bedsStr,
|
|
3932
|
+
String((r.images ?? []).length)
|
|
3933
|
+
];
|
|
3934
|
+
});
|
|
3935
|
+
lines.push(Formatter.table(roomHeaders, roomRows));
|
|
3936
|
+
lines.push(
|
|
3937
|
+
"",
|
|
3938
|
+
Formatter.status(
|
|
3939
|
+
"info",
|
|
3940
|
+
"room_id matches quote's roomItems[].roomId \u2014 use it to relate a room here to its live rate in quote."
|
|
3941
|
+
)
|
|
3942
|
+
);
|
|
3943
|
+
} else {
|
|
3944
|
+
lines.push("");
|
|
3945
|
+
lines.push(Formatter.status("info", "No room-type details available"));
|
|
3946
|
+
}
|
|
3947
|
+
return lines.join("\n");
|
|
3948
|
+
}
|
|
3949
|
+
function registerHotelDetailCommand(parent, deps) {
|
|
3950
|
+
const cmd = parent.command("hotel-detail").description("Get detailed information about a specific hotel (facilities, images, etc.)").option("--api-key <key>", "API Key for authentication (X-Api-Key)").option("--hotel-id <id>", "Hotel ID (required)").option("--with-images [value]", "Include hotel images (default: true)", true);
|
|
3951
|
+
attachSchemaHelp(cmd, hotelDetailSchema);
|
|
3952
|
+
cmd.action(async () => {
|
|
3953
|
+
const opts = cmd.optsWithGlobals();
|
|
3954
|
+
const format = resolveFormat(opts.format);
|
|
3955
|
+
const apiKey = await PromptEngine.resolveInput(opts.apiKey, {
|
|
3956
|
+
message: "API Key:",
|
|
3957
|
+
type: "password"
|
|
3958
|
+
});
|
|
3959
|
+
const hotelId = need15(opts.hotelId, "hotel-id");
|
|
3960
|
+
const rawWithImages = opts.withImages;
|
|
3961
|
+
const withImages = rawWithImages !== "false" && rawWithImages !== false;
|
|
3962
|
+
const body = {
|
|
3963
|
+
hotel_id: hotelId,
|
|
3964
|
+
with_images: withImages
|
|
3965
|
+
};
|
|
3966
|
+
const result = await deps.apiClient.post(
|
|
3967
|
+
"/hotel/detail",
|
|
3968
|
+
{ type: "api-key", key: apiKey },
|
|
3969
|
+
body
|
|
3970
|
+
);
|
|
3971
|
+
if (!result.success) {
|
|
3972
|
+
throw CliError.fromApi(result, { auth: "api-key" });
|
|
1690
3973
|
}
|
|
3974
|
+
const data = result.data;
|
|
1691
3975
|
const configManager = new ConfigManager();
|
|
1692
3976
|
const commandResult = {
|
|
1693
|
-
data
|
|
1694
|
-
text: () =>
|
|
3977
|
+
data,
|
|
3978
|
+
text: () => formatHotelDetail(data)
|
|
1695
3979
|
};
|
|
1696
3980
|
await renderWithContext(commandResult, { format }, configManager);
|
|
1697
3981
|
});
|
|
1698
3982
|
}
|
|
1699
3983
|
|
|
3984
|
+
// src/hotel-redaug/skill.ts
|
|
3985
|
+
var SKILL_TEXT = `
|
|
3986
|
+
---
|
|
3987
|
+
name: hotel-redaug
|
|
3988
|
+
description: Search, create orders, and manage international hotel reservations via the Redaug provider. Two-step create-then-pay flow supporting monthly_settlement and pay_per_call (EVO) billing modes. Use when the user wants to find hotels, check room prices/availability, make a hotel booking, or view/cancel/modify an existing hotel order. Not for flights, rides, or domestic-only stays.
|
|
3989
|
+
---
|
|
3990
|
+
|
|
3991
|
+
# hotel-redaug \u2014 Agent Skill
|
|
3992
|
+
|
|
3993
|
+
You orchestrate hotel bookings through CLI verbs. This guide covers HOW to chain
|
|
3994
|
+
the verbs and pass data between them. For exact flags and field types, read the
|
|
3995
|
+
schema (\`services get svc_01J0HT5REDAUG0001 --api-key <key> --format json\`) or
|
|
3996
|
+
run \`hotel-redaug <verb> --help\`.
|
|
3997
|
+
|
|
3998
|
+
## Prerequisites
|
|
3999
|
+
|
|
4000
|
+
- API key with merchant scope
|
|
4001
|
+
- Two billing modes supported (set on the developer, not chosen per-call):
|
|
4002
|
+
- **monthly_settlement**: developer has a settlement account with sufficient balance
|
|
4003
|
+
- **pay_per_call**: developer collects payment from their end-user via EVO; the user pays using the create-order \`order_id\` as the EVO merchantTransID, and the platform verifies by querying EVO for that order_id (the pay-order response reports settlement_path "pay_per_call")
|
|
4004
|
+
- BILLING_MODE_MISMATCH means the order billing_mode is not one of the two modes above
|
|
4005
|
+
- Pass --api-key <key> --format json on every call; add --yes on writes
|
|
4006
|
+
|
|
4007
|
+
## Workflow
|
|
4008
|
+
|
|
4009
|
+
1. Resolve location
|
|
4010
|
+
- find-destination --keyword "<place>" \u2192 lat/lng or destination_id
|
|
4011
|
+
- list-cities --country <code> \u2192 destination_id
|
|
4012
|
+
- (Need exactly one of these for search.)
|
|
4013
|
+
|
|
4014
|
+
2. Search hotels
|
|
4015
|
+
- Pass ONE location branch (coordinates OR destination_id) + dates + guests
|
|
4016
|
+
- Optional: hotel-filters first to get valid filter codes, then pass to search
|
|
4017
|
+
|
|
4018
|
+
3. Choose a hotel (MANDATORY unless the user already named one exact hotel)
|
|
4019
|
+
- MUST present the candidates to the user (name, address/distance, star/score if useful)
|
|
4020
|
+
and get an explicit pick \u2014 never auto-select by distance or price alone, even for an
|
|
4021
|
+
open-ended request like "book me something near the Bund"
|
|
4022
|
+
- hotel-detail <hotel-id> for the shortlisted/chosen hotel is OPTIONAL AT THIS STEP for
|
|
4023
|
+
the hotel-level info (address/intro/hotel photos) \u2014 the candidate list above is enough
|
|
4024
|
+
to pick a HOTEL. But you WILL need hotel-detail's rooms[] at step 4 below to describe
|
|
4025
|
+
ROOMS, so in practice call it once the user has picked (or shortlisted) a hotel, before
|
|
4026
|
+
moving to quote \u2014 don't make two round trips when one will do
|
|
4027
|
+
|
|
4028
|
+
4. Quote + show room options (ALWAYS call hotel-detail here \u2014 this is the mandatory part)
|
|
4029
|
+
- quote the chosen hotel + same dates/guests \u2192 rates[]
|
|
4030
|
+
- Empty rates[] = no availability \u2192 stop, tell the user
|
|
4031
|
+
- Each rate carries the inputs for create-order: product_token, total_price, price_items
|
|
4032
|
+
- quote's rates[].room_name is only a bare one-line label with NO area/beds/photos. MUST
|
|
4033
|
+
call hotel-detail on the same hotel_id (if not already called in step 3) and match its
|
|
4034
|
+
rooms[] to quote's rates by room_name \u2014 do this proactively, BEFORE presenting rate
|
|
4035
|
+
options, not only if the user asks for more detail. The room is the actual product being
|
|
4036
|
+
booked, so the user picking a rate MUST see area_sqm, floor, beds, and images alongside
|
|
4037
|
+
rate_plan_name/breakfast/free_cancellation/total_price+currency \u2014 never present rate
|
|
4038
|
+
options as a bare list of room_name + price only.
|
|
4039
|
+
- MUST present the combined rate+room options and get the user's explicit pick of which
|
|
4040
|
+
rate to book \u2014 same rule as step 3, and now with real room detail attached, not optional
|
|
4041
|
+
|
|
4042
|
+
5. Create order (lock inventory, no charge) \u2014 MUST NOT run until step 3+4 confirmation happened
|
|
4043
|
+
- create-order: forward quote outputs VERBATIM (product_token, amount, currency, price_items)
|
|
4044
|
+
- Collect from user: guest_name, contact_name, contact_phone
|
|
4045
|
+
- Generate a unique idempotency-key
|
|
4046
|
+
- Response \u2192 save order_id + fc_order_code + total_amount + currency
|
|
4047
|
+
- Order status: AWAITING_PAYMENT (no money charged yet)
|
|
4048
|
+
|
|
4049
|
+
6. BRANCH ON BILLING_MODE \u2014 decide THIS before calling pay-order, not inside it
|
|
4050
|
+
- You (the developer/Agent) know your own billing_mode; the order does not expose it.
|
|
4051
|
+
a) monthly_settlement \u2192 skip straight to step 7 (pay-order). Nothing to do here.
|
|
4052
|
+
b) pay_per_call \u2192 STOP. Do NOT call pay-order yet.
|
|
4053
|
+
- Drive YOUR OWN EVO payment integration now, using the order_id from step 5 AS THE
|
|
4054
|
+
EVO merchantTransID (this is the id you pass to your EVO payment call \u2014 not a new
|
|
4055
|
+
or random id).
|
|
4056
|
+
- Wait for the end-user to actually complete that EVO payment for total_amount+currency.
|
|
4057
|
+
- Only once EVO shows the payment as complete, proceed to step 7 \u2014 pay-order is what
|
|
4058
|
+
tells the platform "go verify and settle this", it does NOT itself collect payment.
|
|
4059
|
+
|
|
4060
|
+
7. Pay order (settle the locked order) \u2014 MUST confirm with the user before calling this
|
|
4061
|
+
- Before calling: show the user total_amount + currency from create-order and which
|
|
4062
|
+
billing path applies, and get explicit go-ahead \u2014 pay-order is the step that actually
|
|
4063
|
+
moves money (for pay_per_call, confirm the EVO payment from step 6 was completed first)
|
|
4064
|
+
- pay-order --order-id <order_id from create-order> (that's the only identifier needed)
|
|
4065
|
+
- Billing path determined server-side by developer's billing_mode:
|
|
4066
|
+
a) monthly_settlement:
|
|
4067
|
+
- Platform deducts from settlement account \u2192 calls upstream payOrder
|
|
4068
|
+
b) pay_per_call:
|
|
4069
|
+
- Requires step 6 to have already happened (user paid via EVO under this order_id).
|
|
4070
|
+
Calling pay-order now just makes the platform query EVO for that order_id and
|
|
4071
|
+
verify exact amount/currency \u2014 it does NOT initiate the EVO payment itself.
|
|
4072
|
+
There is no merchant-transaction-id flag: the EVO key IS the order_id, so a
|
|
4073
|
+
foreign already-paid EVO transaction cannot be substituted.
|
|
4074
|
+
- If EVO not yet confirmed \u2192 PAYMENT_NOT_COMPLETED; use --watch to poll
|
|
4075
|
+
- On success \u2192 order status becomes PAID
|
|
4076
|
+
|
|
4077
|
+
8. Confirm (upstream confirmation is async)
|
|
4078
|
+
- Poll get --order-id until CONFIRMED (success) or CANCELLED (failed)
|
|
4079
|
+
- Do not report success before CONFIRMED
|
|
4080
|
+
|
|
4081
|
+
9. Post-booking (only when the user asks)
|
|
4082
|
+
- cancel: order_id + fc_order_code + reason
|
|
4083
|
+
- checkout: partial leave-early \u2192 then poll get-checkout with task_order_code
|
|
4084
|
+
- list-orders: view all orders
|
|
4085
|
+
|
|
4086
|
+
## Data passing (where each value comes from)
|
|
4087
|
+
|
|
4088
|
+
- product_token : quote \u2192 create-order (opaque; never fabricate or edit)
|
|
4089
|
+
- price_items : quote \u2192 create-order (JSON array; copy exactly)
|
|
4090
|
+
- order_id : create-order \u2192 pay-order / get / cancel
|
|
4091
|
+
- fc_order_code : create-order \u2192 cancel / checkout
|
|
4092
|
+
- order_id (again): for pay_per_call it IS the EVO merchantTransID \u2014 YOU (the developer/Agent)
|
|
4093
|
+
use it to drive your own EVO payment call BEFORE pay-order (see step 6); pay-order itself
|
|
4094
|
+
takes no separate merchant-transaction-id, it only verifies the payment already made
|
|
4095
|
+
- task_order_code : checkout \u2192 get-checkout
|
|
4096
|
+
|
|
4097
|
+
## Billing modes (chosen server-side by billing_mode)
|
|
4098
|
+
|
|
4099
|
+
pay-order takes only --order-id; there is NO merchant-transaction-id flag. The platform
|
|
4100
|
+
routes purely by the order's billing_mode:
|
|
4101
|
+
|
|
4102
|
+
| Developer billing_mode | Behavior |
|
|
4103
|
+
|------------------------|----------|
|
|
4104
|
+
| monthly_settlement | Deducts settlement balance \u2192 payOrder |
|
|
4105
|
+
| pay_per_call | Platform queries EVO for the order_id \u2192 verify exact amount/currency \u2192 payOrder (response settlement_path is "pay_per_call") |
|
|
4106
|
+
| anything else | ERROR: BILLING_MODE_MISMATCH (only the two modes above are valid) |
|
|
4107
|
+
|
|
4108
|
+
## pay_per_call flow \u2014 the EVO merchantTransID IS the order_id
|
|
4109
|
+
|
|
4110
|
+
1. create-order returns order_id + total_amount + currency (no EVO credentials in response)
|
|
4111
|
+
2. YOU drive your own EVO payment integration now, passing the order_id as the EVO
|
|
4112
|
+
merchantTransID, and get the user to pay total_amount+currency
|
|
4113
|
+
(the shared EVO parameters are onboarded separately, out of band \u2014 pay-order does not do
|
|
4114
|
+
this step for you)
|
|
4115
|
+
3. ONLY once that EVO payment is complete, call pay-order --order-id <order_id>
|
|
4116
|
+
(no other identifier)
|
|
4117
|
+
4. Platform queries EVO for that order_id: paid + exact amount/currency match \u2192 payOrder \u2192 PAID
|
|
4118
|
+
5. If not yet paid \u2192 PAYMENT_NOT_COMPLETED; use --watch to auto-poll
|
|
4119
|
+
6. Why order_id (not an arbitrary id): it binds the payment to this exact order, so a caller
|
|
4120
|
+
cannot present some other already-paid EVO transaction of the same amount.
|
|
4121
|
+
|
|
4122
|
+
## Invariants
|
|
4123
|
+
|
|
4124
|
+
- search: exactly one location branch (both or neither = error)
|
|
4125
|
+
- Never create-order without a fresh quote (tokens expire)
|
|
4126
|
+
- Never claim "booked" until get returns CONFIRMED
|
|
4127
|
+
- create-order and pay-order are separate steps; create-order does NOT charge
|
|
4128
|
+
- pay-order depends on create-order's output (order_id)
|
|
4129
|
+
- Write verbs (create-order/pay-order/cancel/checkout) always need --idempotency-key and --yes
|
|
4130
|
+
- IMPORTANT: --yes only skips this CLI's own interactive TTY prompt (needed because the Agent
|
|
4131
|
+
runs non-interactively). It is NOT a substitute for showing the user the hotel/rate/price and
|
|
4132
|
+
getting their decision in the chat UI. Passing --yes does not remove the confirmation steps in
|
|
4133
|
+
step 3/4 (choose hotel + rate) or step 7 (confirm before pay-order) above.
|
|
4134
|
+
- After a cancel call returns successfully (including the cancel_status='cancel_pending' shape),
|
|
4135
|
+
do NOT call cancel again for the same order \u2014 poll get instead. A pending acknowledgement is
|
|
4136
|
+
success, not failure.
|
|
4137
|
+
|
|
4138
|
+
## Errors
|
|
4139
|
+
|
|
4140
|
+
- RESOURCE_NOT_FOUND (find-destination) \u2192 platform 404 for this keyword, distinct from an
|
|
4141
|
+
empty destinations[] (which is a normal no-match, not an error). Do not keep retrying the
|
|
4142
|
+
keyword \u2014 fall back to the coordinate branch (search/hotel-filters --lat/--lng) if you can
|
|
4143
|
+
geocode the place or already know its coordinates; otherwise ask the user for a city or a
|
|
4144
|
+
more specific landmark.
|
|
4145
|
+
- BILLING_MODE_MISMATCH \u2192 order billing_mode is not monthly_settlement or pay_per_call
|
|
4146
|
+
- ACCOUNT_INSUFFICIENT_BALANCE \u2192 settlement account needs top-up; inform user
|
|
4147
|
+
- PAYMENT_NOT_COMPLETED \u2192 EVO not confirmed yet; retry (--watch) or wait
|
|
4148
|
+
- PAYMENT_NOT_FOUND \u2192 no EVO transaction for that ID; verify with user
|
|
4149
|
+
- PAYMENT_AMOUNT_MISMATCH \u2192 EVO amount/currency \u2260 order; user must pay correct amount
|
|
4150
|
+
- PAYORDER_FAILED \u2192 upstream payOrder failed; money refunded, safe to retry
|
|
4151
|
+
- PAYORDER_FAILED_AFTER_PAYMENT \u2192 EVO confirmed but payOrder failed; needs reconciliation
|
|
4152
|
+
- UPSTREAM_ERROR \u2192 transient provider issue; wait and retry once
|
|
4153
|
+
- PARAM_INVALID \u2192 bad argument; run <verb> --help and correct it
|
|
4154
|
+
- empty rates[] \u2192 no rooms; suggest other dates or hotels
|
|
4155
|
+
`.trim();
|
|
4156
|
+
function registerHotelSkillCommand(parent) {
|
|
4157
|
+
parent.command("skill").description("Print the hotel-redaug usage guide (orchestration flow, data passing, rules)").action(() => {
|
|
4158
|
+
console.log(SKILL_TEXT);
|
|
4159
|
+
});
|
|
4160
|
+
}
|
|
4161
|
+
|
|
1700
4162
|
// src/index.ts
|
|
1701
4163
|
var programRef;
|
|
1702
4164
|
async function main() {
|
|
@@ -1704,11 +4166,13 @@ async function main() {
|
|
|
1704
4166
|
await configManager.ensureDirectories();
|
|
1705
4167
|
const apiBaseUrl = await configManager.getApiBaseUrl();
|
|
1706
4168
|
const apiClient = new ApiClient({ baseUrl: apiBaseUrl });
|
|
4169
|
+
const apiHost = await configManager.getApiHost();
|
|
4170
|
+
const discoveryClient = new ApiClient({ baseUrl: apiHost });
|
|
1707
4171
|
const deps = { apiClient };
|
|
1708
4172
|
const program = new Command();
|
|
1709
4173
|
programRef = program;
|
|
1710
4174
|
program.name("agenzo-merchant-cli").version(getCurrentVersion()).description(
|
|
1711
|
-
"Agenzo merchant fulfillment plane: service discovery (services)
|
|
4175
|
+
"Agenzo merchant fulfillment plane: service discovery (services), ride ordering (ride-elife), and hotel booking (hotel-redaug)"
|
|
1712
4176
|
).option("--verbose", "Show verbose logs").option("--yes", "Skip confirmation prompts (for automation/AI Agents)").option(
|
|
1713
4177
|
"--format <format>",
|
|
1714
4178
|
"Output format: json | table (default: json; or set AGENZO_FORMAT)",
|
|
@@ -1725,8 +4189,55 @@ async function main() {
|
|
|
1725
4189
|
registerCancelCommand(rideCmd, deps);
|
|
1726
4190
|
registerListOrdersCommand(rideCmd, deps);
|
|
1727
4191
|
const servicesCmd = program.command("services").description("Merchant service discovery");
|
|
1728
|
-
registerServicesListCommand(servicesCmd);
|
|
1729
|
-
registerServiceGetCommand(servicesCmd);
|
|
4192
|
+
registerServicesListCommand(servicesCmd, { discoveryClient, program });
|
|
4193
|
+
registerServiceGetCommand(servicesCmd, { discoveryClient, program });
|
|
4194
|
+
const hotelCmd = program.command("hotel-redaug").description(
|
|
4195
|
+
`Hotel booking (Redaug) \u2014 international hotel search, quote, create-order, pay-order, cancel, and check-out.
|
|
4196
|
+
|
|
4197
|
+
Workflow (typical order):
|
|
4198
|
+
1. find-destination Resolve a place name \u2192 coordinates (or destination_id via list-cities)
|
|
4199
|
+
2. hotel-filters (optional) Get filter options for a location (star/brand/facility codes)
|
|
4200
|
+
3. search Search hotels by coordinates OR destination_id, with date/guest/filter params
|
|
4201
|
+
4. hotel-detail View hotel + rooms[] (area/floor/beds/photos). Call it for the chosen/
|
|
4202
|
+
shortlisted hotel BEFORE quote \u2014 quote's room_name alone has no detail,
|
|
4203
|
+
and the user should see real room info before picking a rate at step 5.
|
|
4204
|
+
5. quote Get real-time room rates for a hotel + dates \u2192 product_token + price_items.
|
|
4205
|
+
Pair each rate with hotel-detail's rooms[] (match by room_name) before
|
|
4206
|
+
presenting options to the user.
|
|
4207
|
+
6. create-order Create order using product_token from quote (lock inventory, no charge)
|
|
4208
|
+
7. pay-order Settle the order by --order-id (path chosen server-side by billing_mode)
|
|
4209
|
+
8. get Poll order status until CONFIRMED (or use --watch for NDJSON stream)
|
|
4210
|
+
9. cancel Cancel an order (whole-order, within policy)
|
|
4211
|
+
10. checkout Apply for partial check-out / out-of-policy cancellation (async)
|
|
4212
|
+
11. get-checkout Poll checkout application status
|
|
4213
|
+
12. list-orders List developer's hotel orders
|
|
4214
|
+
|
|
4215
|
+
Key notes:
|
|
4216
|
+
\u2022 search has two location branches: --destination-id OR --lat/--lng (exactly one required)
|
|
4217
|
+
\u2022 hotel-detail's rooms[] is the actual room info (area/beds/photos) \u2014 quote's room_name is
|
|
4218
|
+
just a bare label; show both together before asking the user to pick a rate
|
|
4219
|
+
\u2022 create-order locks inventory without charging; order enters AWAITING_PAYMENT
|
|
4220
|
+
\u2022 pay-order settles the order by --order-id; the path is chosen server-side by
|
|
4221
|
+
billing_mode. For pay_per_call the user pays via EVO using the order_id
|
|
4222
|
+
as the merchantTransID first; use --watch to poll until PAID
|
|
4223
|
+
\u2022 All amounts are DECIMAL (e.g. 10.00 = ten yuan), never minor units (cents)
|
|
4224
|
+
\u2022 product_token from quote is opaque \u2014 pass it unchanged to create-order
|
|
4225
|
+
\u2022 price_items from quote must be copied verbatim as --price-items JSON array to create-order`
|
|
4226
|
+
);
|
|
4227
|
+
registerHotelSearchCommand(hotelCmd, deps);
|
|
4228
|
+
registerHotelQuoteCommand(hotelCmd, deps);
|
|
4229
|
+
registerHotelCreateOrderCommand(hotelCmd, deps);
|
|
4230
|
+
registerHotelPayOrderCommand(hotelCmd, deps);
|
|
4231
|
+
registerHotelGetCommand(hotelCmd, deps);
|
|
4232
|
+
registerHotelCancelCommand(hotelCmd, deps);
|
|
4233
|
+
registerHotelCheckoutCommand(hotelCmd, deps);
|
|
4234
|
+
registerHotelGetCheckoutCommand(hotelCmd, deps);
|
|
4235
|
+
registerHotelListOrdersCommand(hotelCmd, deps);
|
|
4236
|
+
registerHotelFindDestinationCommand(hotelCmd, deps);
|
|
4237
|
+
registerHotelFiltersCommand(hotelCmd, deps);
|
|
4238
|
+
registerHotelListCitiesCommand(hotelCmd, deps);
|
|
4239
|
+
registerHotelDetailCommand(hotelCmd, deps);
|
|
4240
|
+
registerHotelSkillCommand(hotelCmd);
|
|
1730
4241
|
await program.parseAsync(process.argv);
|
|
1731
4242
|
}
|
|
1732
4243
|
function resolveActiveFormat() {
|
|
@@ -1742,6 +4253,9 @@ function reportError(error) {
|
|
|
1742
4253
|
console.error(
|
|
1743
4254
|
Formatter.status("error", `[${envelope.error.code_num}] ${envelope.error.message}`)
|
|
1744
4255
|
);
|
|
4256
|
+
if (envelope.error.upstream) {
|
|
4257
|
+
console.error(` \u21B3 [${envelope.error.upstream.code}] ${envelope.error.upstream.message}`);
|
|
4258
|
+
}
|
|
1745
4259
|
if (!(error instanceof CliError) && process.argv.includes("--verbose")) {
|
|
1746
4260
|
console.error(error);
|
|
1747
4261
|
}
|