@agenzo/merchant-cli 0.4.3 → 0.5.0

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