@ketvietlab/ketsuite 0.1.3 → 0.1.5

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.
Files changed (191) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/deployment.d.ts.map +1 -1
  3. package/dist/deployment.js +3 -0
  4. package/dist/deployment.js.map +1 -1
  5. package/dist/index.d.ts +7 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +6 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/modules/account_staff_channel/channel-routes.d.ts.map +1 -1
  10. package/dist/modules/account_staff_channel/channel-routes.js +1 -12
  11. package/dist/modules/account_staff_channel/channel-routes.js.map +1 -1
  12. package/dist/modules/attendance/channel-routes.d.ts.map +1 -1
  13. package/dist/modules/attendance/channel-routes.js +1 -12
  14. package/dist/modules/attendance/channel-routes.js.map +1 -1
  15. package/dist/modules/backend/design/client/table-selection.d.mts +3 -3
  16. package/dist/modules/backend/design/client/table-selection.d.mts.map +1 -1
  17. package/dist/modules/backend/design/client/table-selection.mjs +1 -1
  18. package/dist/modules/backend/design/client/table-selection.mjs.map +3 -3
  19. package/dist/modules/backend/design/content.css +43 -0
  20. package/dist/modules/backend/design/forms.css +22 -0
  21. package/dist/modules/backend/design/responsive.css +0 -5
  22. package/dist/modules/backend/forms.d.ts +38 -1
  23. package/dist/modules/backend/forms.d.ts.map +1 -1
  24. package/dist/modules/backend/forms.js +29 -0
  25. package/dist/modules/backend/forms.js.map +1 -1
  26. package/dist/modules/backend/index.d.ts +3 -2
  27. package/dist/modules/backend/index.d.ts.map +1 -1
  28. package/dist/modules/backend/index.js +2 -2
  29. package/dist/modules/backend/index.js.map +1 -1
  30. package/dist/modules/backend/messages.d.ts.map +1 -1
  31. package/dist/modules/backend/messages.js +40 -2
  32. package/dist/modules/backend/messages.js.map +1 -1
  33. package/dist/modules/channel_api/core.d.ts +30 -0
  34. package/dist/modules/channel_api/core.d.ts.map +1 -1
  35. package/dist/modules/channel_api/core.js +28 -0
  36. package/dist/modules/channel_api/core.js.map +1 -1
  37. package/dist/modules/crm_staff_channel/channel-routes.d.ts.map +1 -1
  38. package/dist/modules/crm_staff_channel/channel-routes.js +1 -12
  39. package/dist/modules/crm_staff_channel/channel-routes.js.map +1 -1
  40. package/dist/modules/flow/membership.d.ts.map +1 -1
  41. package/dist/modules/flow/membership.js +7 -1
  42. package/dist/modules/flow/membership.js.map +1 -1
  43. package/dist/modules/flow/operations.d.ts +10 -4
  44. package/dist/modules/flow/operations.d.ts.map +1 -1
  45. package/dist/modules/flow/operations.js +10 -4
  46. package/dist/modules/flow/operations.js.map +1 -1
  47. package/dist/modules/flow_staff_channel/channel-routes.d.ts.map +1 -1
  48. package/dist/modules/flow_staff_channel/channel-routes.js +1 -20
  49. package/dist/modules/flow_staff_channel/channel-routes.js.map +1 -1
  50. package/dist/modules/hospitality_core/messages.d.ts.map +1 -1
  51. package/dist/modules/hospitality_core/messages.js +44 -0
  52. package/dist/modules/hospitality_core/messages.js.map +1 -1
  53. package/dist/modules/hospitality_core/night-audit.d.ts.map +1 -1
  54. package/dist/modules/hospitality_core/night-audit.js +7 -0
  55. package/dist/modules/hospitality_core/night-audit.js.map +1 -1
  56. package/dist/modules/hospitality_core/operations.d.ts.map +1 -1
  57. package/dist/modules/hospitality_core/operations.js +150 -40
  58. package/dist/modules/hospitality_core/operations.js.map +1 -1
  59. package/dist/modules/hospitality_core/routes.d.ts.map +1 -1
  60. package/dist/modules/hospitality_core/routes.js +24 -2
  61. package/dist/modules/hospitality_core/routes.js.map +1 -1
  62. package/dist/modules/hospitality_core/screens/check-out-prep.d.ts +46 -0
  63. package/dist/modules/hospitality_core/screens/check-out-prep.d.ts.map +1 -0
  64. package/dist/modules/hospitality_core/screens/check-out-prep.js +133 -0
  65. package/dist/modules/hospitality_core/screens/check-out-prep.js.map +1 -0
  66. package/dist/modules/hospitality_core/screens/folio-detail.d.ts.map +1 -1
  67. package/dist/modules/hospitality_core/screens/folio-detail.js +2 -4
  68. package/dist/modules/hospitality_core/screens/folio-detail.js.map +1 -1
  69. package/dist/modules/hospitality_core/screens/index.d.ts +2 -0
  70. package/dist/modules/hospitality_core/screens/index.d.ts.map +1 -1
  71. package/dist/modules/hospitality_core/screens/index.js +1 -0
  72. package/dist/modules/hospitality_core/screens/index.js.map +1 -1
  73. package/dist/modules/hospitality_core/screens/reservation-detail.d.ts.map +1 -1
  74. package/dist/modules/hospitality_core/screens/reservation-detail.js +8 -1
  75. package/dist/modules/hospitality_core/screens/reservation-detail.js.map +1 -1
  76. package/dist/modules/hospitality_core/screens/shared.d.ts +12 -0
  77. package/dist/modules/hospitality_core/screens/shared.d.ts.map +1 -1
  78. package/dist/modules/hospitality_core/screens/shared.js +25 -4
  79. package/dist/modules/hospitality_core/screens/shared.js.map +1 -1
  80. package/dist/modules/hospitality_core/services.d.ts +48 -0
  81. package/dist/modules/hospitality_core/services.d.ts.map +1 -1
  82. package/dist/modules/hospitality_core/services.js +70 -0
  83. package/dist/modules/hospitality_core/services.js.map +1 -1
  84. package/dist/modules/hospitality_staff_channel/channel-routes.d.ts.map +1 -1
  85. package/dist/modules/hospitality_staff_channel/channel-routes.js +1 -12
  86. package/dist/modules/hospitality_staff_channel/channel-routes.js.map +1 -1
  87. package/dist/modules/inventory_staff_channel/channel-routes.d.ts.map +1 -1
  88. package/dist/modules/inventory_staff_channel/channel-routes.js +1 -12
  89. package/dist/modules/inventory_staff_channel/channel-routes.js.map +1 -1
  90. package/dist/modules/purchase_staff_channel/order-routes.d.ts.map +1 -1
  91. package/dist/modules/purchase_staff_channel/order-routes.js +1 -12
  92. package/dist/modules/purchase_staff_channel/order-routes.js.map +1 -1
  93. package/dist/modules/quality_staff_channel/channel-routes.d.ts.map +1 -1
  94. package/dist/modules/quality_staff_channel/channel-routes.js +1 -12
  95. package/dist/modules/quality_staff_channel/channel-routes.js.map +1 -1
  96. package/dist/modules/sale/functions.d.ts.map +1 -1
  97. package/dist/modules/sale/functions.js +27 -17
  98. package/dist/modules/sale/functions.js.map +1 -1
  99. package/dist/modules/sale_staff_channel/channel-routes.d.ts.map +1 -1
  100. package/dist/modules/sale_staff_channel/channel-routes.js +1 -12
  101. package/dist/modules/sale_staff_channel/channel-routes.js.map +1 -1
  102. package/dist/modules/sale_staff_channel/order-routes.d.ts.map +1 -1
  103. package/dist/modules/sale_staff_channel/order-routes.js +1 -12
  104. package/dist/modules/sale_staff_channel/order-routes.js.map +1 -1
  105. package/dist/modules/stock_staff_channel/operation-routes.d.ts.map +1 -1
  106. package/dist/modules/stock_staff_channel/operation-routes.js +1 -12
  107. package/dist/modules/stock_staff_channel/operation-routes.js.map +1 -1
  108. package/dist/modules/user_backend/messages.d.ts +4 -0
  109. package/dist/modules/user_backend/messages.d.ts.map +1 -1
  110. package/dist/modules/user_backend/messages.js +4 -0
  111. package/dist/modules/user_backend/messages.js.map +1 -1
  112. package/dist/modules/user_backend/routes.d.ts.map +1 -1
  113. package/dist/modules/user_backend/routes.js +28 -1
  114. package/dist/modules/user_backend/routes.js.map +1 -1
  115. package/dist/modules/user_backend/screens/user-form.d.ts +1 -0
  116. package/dist/modules/user_backend/screens/user-form.d.ts.map +1 -1
  117. package/dist/modules/user_backend/screens/user-form.js +28 -0
  118. package/dist/modules/user_backend/screens/user-form.js.map +1 -1
  119. package/dist/modules/website/cms.d.ts.map +1 -1
  120. package/dist/modules/website/cms.js +283 -6
  121. package/dist/modules/website/cms.js.map +1 -1
  122. package/dist/modules/website/index.d.ts.map +1 -1
  123. package/dist/modules/website/index.js +12 -0
  124. package/dist/modules/website/index.js.map +1 -1
  125. package/dist/modules/website/media-usage.d.ts +38 -0
  126. package/dist/modules/website/media-usage.d.ts.map +1 -0
  127. package/dist/modules/website/media-usage.js +87 -0
  128. package/dist/modules/website/media-usage.js.map +1 -0
  129. package/dist/modules/website/paths.js +1 -1
  130. package/dist/modules/website/paths.js.map +1 -1
  131. package/dist/modules/website/sections.d.ts.map +1 -1
  132. package/dist/modules/website/sections.js +12 -1
  133. package/dist/modules/website/sections.js.map +1 -1
  134. package/dist/modules/website_backend/index.js +138 -0
  135. package/dist/modules/website_backend/index.js.map +1 -1
  136. package/dist/modules/website_backend/menus.d.ts.map +1 -1
  137. package/dist/modules/website_backend/menus.js +11 -0
  138. package/dist/modules/website_backend/menus.js.map +1 -1
  139. package/dist/modules/website_backend/routes.d.ts.map +1 -1
  140. package/dist/modules/website_backend/routes.js +414 -31
  141. package/dist/modules/website_backend/routes.js.map +1 -1
  142. package/dist/modules/website_backend/screens/index.d.ts +98 -6
  143. package/dist/modules/website_backend/screens/index.d.ts.map +1 -1
  144. package/dist/modules/website_backend/screens/index.js +600 -154
  145. package/dist/modules/website_backend/screens/index.js.map +1 -1
  146. package/dist/modules/website_backend/screens/page-frame.d.ts +3 -1
  147. package/dist/modules/website_backend/screens/page-frame.d.ts.map +1 -1
  148. package/dist/modules/website_backend/screens/page-frame.js +1 -1
  149. package/dist/modules/website_backend/screens/page-frame.js.map +1 -1
  150. package/dist/modules/website_menu/functions.d.ts.map +1 -1
  151. package/dist/modules/website_menu/functions.js +51 -0
  152. package/dist/modules/website_menu/functions.js.map +1 -1
  153. package/dist/modules/website_menu/index.js +2 -0
  154. package/dist/modules/website_menu/index.js.map +1 -1
  155. package/dist/modules/website_search/functions.d.ts.map +1 -1
  156. package/dist/modules/website_search/functions.js +10 -120
  157. package/dist/modules/website_search/functions.js.map +1 -1
  158. package/dist/modules/website_search/index.js +2 -0
  159. package/dist/modules/website_search/index.js.map +1 -1
  160. package/dist/modules/website_search/jobs.d.ts +3 -0
  161. package/dist/modules/website_search/jobs.d.ts.map +1 -0
  162. package/dist/modules/website_search/jobs.js +101 -0
  163. package/dist/modules/website_search/jobs.js.map +1 -0
  164. package/dist/modules/website_search/rebuild.d.ts +33 -0
  165. package/dist/modules/website_search/rebuild.d.ts.map +1 -0
  166. package/dist/modules/website_search/rebuild.js +125 -0
  167. package/dist/modules/website_search/rebuild.js.map +1 -0
  168. package/dist/permission-catalogue.d.ts.map +1 -1
  169. package/dist/permission-catalogue.js +14 -1
  170. package/dist/permission-catalogue.js.map +1 -1
  171. package/dist/scaffold/index.js +1 -1
  172. package/dist/ui/chrome.d.ts +8 -0
  173. package/dist/ui/chrome.d.ts.map +1 -1
  174. package/dist/ui/chrome.js +9 -1
  175. package/dist/ui/chrome.js.map +1 -1
  176. package/dist/ui/client/table-selection-view.d.ts.map +1 -1
  177. package/dist/ui/client/table-selection-view.js +67 -5
  178. package/dist/ui/client/table-selection-view.js.map +1 -1
  179. package/dist/ui/form.d.ts +2 -0
  180. package/dist/ui/form.d.ts.map +1 -1
  181. package/dist/ui/form.js +1 -0
  182. package/dist/ui/form.js.map +1 -1
  183. package/dist/ui/index.d.ts +2 -2
  184. package/dist/ui/index.d.ts.map +1 -1
  185. package/dist/ui/index.js +2 -2
  186. package/dist/ui/index.js.map +1 -1
  187. package/dist/ui/state.d.ts +20 -1
  188. package/dist/ui/state.d.ts.map +1 -1
  189. package/dist/ui/state.js +17 -0
  190. package/dist/ui/state.js.map +1 -1
  191. package/package.json +4 -4
@@ -3,7 +3,7 @@ import { addDecimals, canonicalDecimalText, compareDecimals, moneyMinor, minorTe
3
3
  import { BILLING_MODES, BOOKING_PROVIDERS, BOOKING_TYPES, DOCUMENT_TYPES, GENDERS, OCR_STATES, OUT_OF_SERVICE_ROOM_STATUSES, } from './types.js';
4
4
  import { dateKeyIn } from './calendar.js';
5
5
  import { defaultRatePlan, hourlyAvailable, InventoryConflict, occupancyDates, quoteAvailability, recordInventoryChange, releaseInventory, replaceReservedInventory, reserveInventory, restrictionIssues, } from './inventory.js';
6
- import { postCharge } from './services.js';
6
+ import { FolioConflict, postCharge, settleCancelledFolio } from './services.js';
7
7
  import { initializeRecurringRent } from './night-audit.js';
8
8
  const issue = (field, code, params) => ({
9
9
  field,
@@ -150,6 +150,8 @@ const transition = async (run) => {
150
150
  catch (error) {
151
151
  if (error instanceof TransitionConflict)
152
152
  return failure(error.problem);
153
+ if (error instanceof FolioConflict)
154
+ return failure(error.problem);
153
155
  if (error instanceof InventoryConflict)
154
156
  return failure(error.problem);
155
157
  throw error;
@@ -391,6 +393,11 @@ const voidPostedCharge = async (ctx, args) => {
391
393
  export const applyNoShow = async (ctx, reservation, reason, at) => {
392
394
  const timestamp = at.toISOString();
393
395
  const property = await record(ctx, 'hospitality_core.Property', reservation.propertyId);
396
+ // A guest who never arrived owes what the cancellation policy says, not the
397
+ // whole stay. The folio used to close with every room night still active on
398
+ // it, so a flexible booking nobody showed up for was billed in full — a
399
+ // no-show is the cancellation the guest never got around to making.
400
+ const fee = await cancellationFee(ctx, reservation, at);
394
401
  const inventoryDates = reservation.bookingType === 'hourly'
395
402
  ? []
396
403
  : occupancyDates(reservation.checkIn, reservation.checkOut, String(property?.timezone ?? 'UTC'));
@@ -407,9 +414,16 @@ export const applyNoShow = async (ctx, reservation, reason, at) => {
407
414
  const folio = await record(tx, 'hospitality_core.Folio', reservation.folioId);
408
415
  if (folio?.state !== 'open')
409
416
  throw new TransitionConflict(issue('folioId', 'folio_not_open'));
410
- const folioClaim = await tx.db.compareAndSet('hospitality_core.Folio', { id: folio.id }, { state: 'open', version: folio.version }, { state: 'closed', closedAt: timestamp, version: Number(folio.version) + 1 });
411
- if (!('matched' in folioClaim) || !folioClaim.matched)
412
- throw new TransitionConflict(issue('folioId', 'transition_conflict'));
417
+ await settleCancelledFolio(tx, {
418
+ folioId: folio.id,
419
+ stayId: reservation.stayId,
420
+ fee: fee.amount,
421
+ chargeId: `${String(reservation.id)}:no_show`,
422
+ sourceKey: `reservation:${String(reservation.id)}:no_show`,
423
+ reason: fee.code,
424
+ kind: 'no_show',
425
+ at: timestamp,
426
+ });
413
427
  if (inventoryDates.length) {
414
428
  await releaseInventory(tx, reservation.propertyId, reservation.roomTypeId, inventoryDates, Number(reservation.roomQuantity ?? 1));
415
429
  await recordInventoryChange(tx, {
@@ -421,17 +435,9 @@ export const applyNoShow = async (ctx, reservation, reason, at) => {
421
435
  aggregateId: reservation.id,
422
436
  });
423
437
  }
424
- return success(reservation.id, { state: 'no_show' });
438
+ return success(reservation.id, { state: 'no_show', noShowFee: fee.amount });
425
439
  });
426
440
  };
427
- /**
428
- * A room is kept for one stay at a time over the nights it is kept for.
429
- *
430
- * Room `status` cannot carry this. A room held for next Tuesday is an ordinary
431
- * available room today and must stay sellable; `occupied` is about now, not
432
- * about a calendar. So the exclusion lives in the assignment schedule, and this
433
- * is the question every writer of one has to ask first.
434
- */
435
441
  /**
436
442
  * Put the room's own name on the assignments that carry only its id.
437
443
  *
@@ -455,6 +461,14 @@ const nameAssignedRooms = async (ctx, stays) => {
455
461
  assignment.roomName = names.get(String(assignment.roomId)) ?? assignment.roomName;
456
462
  return stays;
457
463
  };
464
+ /**
465
+ * A room is kept for one stay at a time over the nights it is kept for.
466
+ *
467
+ * Room `status` cannot carry this. A room held for next Tuesday is an ordinary
468
+ * available room today and must stay sellable; `occupied` is about now, not
469
+ * about a calendar. So the exclusion lives in the assignment schedule, and this
470
+ * is the question every writer of one has to ask first.
471
+ */
458
472
  const roomTakenBetween = async (ctx, roomId, startAt, endAt, exceptStayId) => {
459
473
  const A = ctx.table('hospitality_core.RoomAssignment');
460
474
  const rows = await ctx.db.all(from(A).where(eq(A.roomId, roomId)));
@@ -930,32 +944,14 @@ export const operations = {
930
944
  throw new TransitionConflict(issue('state', 'transition_conflict'));
931
945
  await closeHold(tx, reservation.stayId, 'room_hold_cancelled');
932
946
  }
933
- const folio = await record(tx, 'hospitality_core.Folio', reservation.folioId);
934
- const C = tx.table('hospitality_core.Charge');
935
- const charges = await tx.db.all(from(C).where(eq(C.folioId, reservation.folioId), eq(C.state, 'active')));
936
- for (const charge of charges)
937
- await tx.db.update('hospitality_core.Charge', { id: charge.id }, { state: 'void' });
938
- if (compareDecimals(fee.amount, '0') > 0)
939
- await tx.db.insert('hospitality_core.Charge', {
940
- id: `${String(reservation.id)}:cancellation`,
941
- folioId: reservation.folioId,
942
- stayId: reservation.stayId,
943
- description: `cancellation:${fee.code}`,
944
- type: 'cancellation',
945
- quantity: '1',
946
- unitPrice: fee.amount,
947
- amount: fee.amount,
948
- occurredAt: at,
949
- sourceKey: `reservation:${String(reservation.id)}:cancellation`,
950
- state: 'active',
951
- });
952
- // Money owed does not vanish because the stay did. A folio carrying a
953
- // penalty closes; only a folio owing nothing is cancelled outright.
954
- await tx.db.update('hospitality_core.Folio', { id: reservation.folioId }, {
955
- state: compareDecimals(fee.amount, '0') > 0 ? 'closed' : 'cancelled',
956
- amountTotal: fee.amount,
957
- closedAt: at,
958
- version: Number(folio?.version ?? 0) + 1,
947
+ await settleCancelledFolio(tx, {
948
+ folioId: reservation.folioId,
949
+ stayId: reservation.stayId,
950
+ fee: fee.amount,
951
+ chargeId: `${String(reservation.id)}:cancellation`,
952
+ sourceKey: `reservation:${String(reservation.id)}:cancellation`,
953
+ reason: fee.code,
954
+ at,
959
955
  });
960
956
  if (inventoryDates.length)
961
957
  await releaseInventory(tx, reservation.propertyId, reservation.roomTypeId, inventoryDates, Number(reservation.roomQuantity ?? 1));
@@ -974,16 +970,21 @@ export const operations = {
974
970
  }),
975
971
  markNoShow: defineFn({
976
972
  input: { id: 'id', reason: 'text', at: 'datetime?' },
977
- output: { ok: 'bool', id: 'id?', state: 'text?', errors: 'json?' },
973
+ output: { ok: 'bool', id: 'id?', state: 'text?', noShowFee: 'decimal?', errors: 'json?' },
978
974
  effects: [
979
975
  'read:hospitality_core.Reservation',
976
+ 'read:hospitality_core.Charge',
980
977
  'read:hospitality_core.Folio',
981
978
  'read:hospitality_core.Property',
979
+ 'read:hospitality_core.RoomType',
980
+ 'read:hospitality_core.CancellationPolicy',
981
+ 'read:company.Company',
982
982
  'read:hospitality_core.Room',
983
983
  'read:hospitality_core.AvailabilityLedger',
984
984
  'write:hospitality_core.Reservation',
985
985
  'write:hospitality_core.Stay',
986
986
  'write:hospitality_core.Folio',
987
+ 'write:hospitality_core.Charge',
987
988
  'write:hospitality_core.AvailabilityLedger',
988
989
  'write:hospitality_core.InventoryChange',
989
990
  'read:hospitality_core.RoomAssignment',
@@ -1695,6 +1696,115 @@ export const operations = {
1695
1696
  return ctx.db.all(query);
1696
1697
  },
1697
1698
  }),
1699
+ /**
1700
+ * Everything the desk needs before it presses check out, in one read.
1701
+ *
1702
+ * A departure was four screens: the stay to see who and which room, the folio
1703
+ * to see what is owed, the extras to see what a supplier has not confirmed
1704
+ * yet, and the property to know whether the hour matters. Nothing joined
1705
+ * them, so whether a guest could leave was a judgement each person made by
1706
+ * hand — and the refusal only arrived after the button.
1707
+ *
1708
+ * The blockers here are the same ones `checkOut` raises, read rather than
1709
+ * discovered. `balance` is deliberately not one of them: leaving is not
1710
+ * paying, and a hotel that refused every departure with a bill outstanding
1711
+ * would refuse most of them.
1712
+ */
1713
+ checkOutReadiness: defineFn({
1714
+ input: { stayId: 'id' },
1715
+ output: {
1716
+ stayId: 'id',
1717
+ state: 'text',
1718
+ ready: 'bool',
1719
+ roomId: 'id?',
1720
+ roomName: 'text?',
1721
+ guestName: 'text?',
1722
+ checkIn: 'datetime?',
1723
+ dueOut: 'datetime?',
1724
+ late: 'bool',
1725
+ lateReasonRequired: 'bool',
1726
+ folioId: 'id?',
1727
+ folioState: 'text?',
1728
+ balance: 'decimal?',
1729
+ charges: 'json?',
1730
+ awaitingFulfilment: 'json?',
1731
+ blockers: 'json?',
1732
+ },
1733
+ effects: [
1734
+ 'read:hospitality_core.Stay',
1735
+ 'read:hospitality_core.Property',
1736
+ 'read:hospitality_core.Room',
1737
+ 'read:hospitality_core.RoomAssignment',
1738
+ 'read:hospitality_core.Folio',
1739
+ 'read:hospitality_core.Charge',
1740
+ 'read:hospitality_core.ExtraLine',
1741
+ 'read:partner.Partner',
1742
+ ],
1743
+ agent: true,
1744
+ handler: async (ctx, args) => {
1745
+ const stay = await record(ctx, 'hospitality_core.Stay', args.stayId);
1746
+ if (!stay)
1747
+ return {
1748
+ stayId: args.stayId,
1749
+ state: 'missing',
1750
+ ready: false,
1751
+ late: false,
1752
+ lateReasonRequired: false,
1753
+ blockers: [issue('stayId', 'stay_missing')],
1754
+ };
1755
+ const property = await record(ctx, 'hospitality_core.Property', stay.propertyId);
1756
+ const room = stay.currentRoomId ? await record(ctx, 'hospitality_core.Room', stay.currentRoomId) : null;
1757
+ const partner = stay.partnerId ? await record(ctx, 'partner.Partner', stay.partnerId) : null;
1758
+ const folio = stay.folioId ? await record(ctx, 'hospitality_core.Folio', stay.folioId) : null;
1759
+ const now = new Date();
1760
+ const dueOut = date(stay.checkOut);
1761
+ const late = !!dueOut && now > dueOut;
1762
+ const blockers = [];
1763
+ if (stay.state === 'checked_out')
1764
+ blockers.push(issue('state', 'stay_already_checked_out'));
1765
+ else if (stay.state !== 'checked_in' || !stay.currentRoomId)
1766
+ blockers.push(issue('state', 'stay_cannot_check_out'));
1767
+ if (!property)
1768
+ blockers.push(issue('propertyId', 'property_missing'));
1769
+ const A = ctx.table('hospitality_core.RoomAssignment');
1770
+ const active = await ctx.db.one(from(A).where(eq(A.stayId, stay.id), eq(A.state, 'active')));
1771
+ if (stay.state === 'checked_in' && !active)
1772
+ blockers.push(issue('stayId', 'active_assignment_missing'));
1773
+ const C = ctx.table('hospitality_core.Charge');
1774
+ const charges = folio
1775
+ ? await ctx.db.all(from(C)
1776
+ .where(eq(C.folioId, folio.id), eq(C.state, 'active'))
1777
+ .orderBy(asc(C.occurredAt), asc(C.id)))
1778
+ : [];
1779
+ // A line a supplier still owes us is not a refusal, but closing the folio
1780
+ // over it is how a charge goes missing. The desk gets to see it first.
1781
+ const E = ctx.table('hospitality_core.ExtraLine');
1782
+ const lines = await ctx.db.all(from(E).where(eq(E.stayId, stay.id), eq(E.active, true)).preload('charges'));
1783
+ const awaitingFulfilment = lines
1784
+ .filter((line) => String(line.fulfillmentKind ?? 'none') === 'external_stock')
1785
+ .filter((line) => !(line.charges ?? []).some((charge) => charge.state === 'active'))
1786
+ .map((line) => ({ id: line.id, description: line.description, quantity: line.quantity }));
1787
+ const lateReasonRequired = property?.enforceTimes === true && late;
1788
+ return {
1789
+ stayId: stay.id,
1790
+ state: String(stay.state),
1791
+ ready: blockers.length === 0,
1792
+ roomId: stay.currentRoomId ?? null,
1793
+ roomName: room ? String(room.name ?? room.code ?? room.id) : null,
1794
+ guestName: partner ? String(partner.name ?? '') : null,
1795
+ checkIn: stay.checkIn ?? null,
1796
+ dueOut: stay.checkOut ?? null,
1797
+ late,
1798
+ lateReasonRequired,
1799
+ folioId: folio?.id ?? null,
1800
+ folioState: folio ? String(folio.state) : null,
1801
+ balance: folio ? String(folio.amountTotal ?? '0') : null,
1802
+ charges,
1803
+ awaitingFulfilment,
1804
+ blockers,
1805
+ };
1806
+ },
1807
+ }),
1698
1808
  getFolio: defineFn({
1699
1809
  input: { id: 'id' },
1700
1810
  output: {