@d19n/youfibre-odin-sdk 2.0.157 → 2.0.158

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.
@@ -89,6 +89,20 @@ export interface YfKciEffect {
89
89
  readonly timeBlockFrom: string;
90
90
  /** JSONPath for the supplier-side appointment reservation reference. */
91
91
  readonly supplierReferenceFrom: string;
92
+ /**
93
+ * When set, this dispatch is a *reschedule* (not a first-time booking):
94
+ * the dispatcher passes this value as the schedule-appointment
95
+ * `RescheduleReason`, which makes field-service update the existing
96
+ * ServiceAppointment in place AND record a `SA_RESCHEDULE` ChangeReason
97
+ * (Owner derived from the reason; supplier reasons → SUPPLIER).
98
+ *
99
+ * NOTE: `ChangeReasonRescheduleReason` does NOT map 1:1 to KCI types —
100
+ * the KCI only tells us *that* the supplier moved the slot, not the
101
+ * granular operational reason. We therefore stamp a single neutral
102
+ * supplier-owned value (`SCHEDULE_CHANGED`) for every supplier-driven
103
+ * KCI reschedule rather than inventing a per-KCI mapping.
104
+ */
105
+ readonly rescheduleReason?: string;
92
106
  };
93
107
  readonly notes: string;
94
108
  }
@@ -139,6 +153,13 @@ export declare type KciDispatchDecision = {
139
153
  entityKey: YfKciTargetEntity;
140
154
  properties: ReadonlyArray<string>;
141
155
  };
156
+ /**
157
+ * Present only for reschedule rows. The dispatcher forwards it as the
158
+ * schedule-appointment `RescheduleReason`, triggering in-place SA update
159
+ * + `SA_RESCHEDULE` ChangeReason creation. Absent for first-booking-style
160
+ * schedule-appointment effects.
161
+ */
162
+ rescheduleReason?: string;
142
163
  } | {
143
164
  kind: 'PROPERTY_WRITE';
144
165
  row: KciEffectRow;
@@ -189,9 +189,16 @@ exports.KCI_EFFECT_ROWS = [
189
189
  yfEffect: {
190
190
  kind: 'WO_SCHEDULE_APPOINTMENT',
191
191
  scheduleAppointment: {
192
- dateFrom: 'provideServiceOrder.appointmentTimeslot.timeslotStartDateTime',
193
- timeBlockFrom: 'provideServiceOrder.appointmentTimeslot.classification',
192
+ // NET now stamps the rebooked slot on the flat KCI envelope fields
193
+ // (appointmentDate / appointmentTimeBlock) rather than the nested
194
+ // provideServiceOrder.appointmentTimeslot. appointmentTimeBlock is
195
+ // already the AM/PM block — no WEEKDAY_ prefix to trim.
196
+ dateFrom: 'appointmentDate',
197
+ timeBlockFrom: 'appointmentTimeBlock',
194
198
  supplierReferenceFrom: 'provideServiceOrder.appointmentReservationReference',
199
+ // REAPPOINTED is a supplier reschedule → update existing SA in place
200
+ // + record the SA_RESCHEDULE ChangeReason.
201
+ rescheduleReason: 'SCHEDULE_CHANGED',
195
202
  },
196
203
  // Co-effect: REAPPOINTED's dto also carries an updated
197
204
  // `provideServiceOrder.committedDate` for the rebooked appointment.
@@ -321,6 +328,30 @@ exports.KCI_EFFECT_ROWS = [
321
328
  },
322
329
  logAgainstOrder: true,
323
330
  },
331
+ {
332
+ // Supplier rescheduled the REMEDIATION visit. Move the REMEDIATION WO's
333
+ // appointment to the new slot and land it back in Scheduled, recording an
334
+ // SA_RESCHEDULE ChangeReason. Emitted on the parent INSTALL's PROVIDE
335
+ // order; tenantWorkOrderReference points at the REMEDIATION WO.
336
+ id: 'provide-update-remediation-rescheduled',
337
+ reasonCode: 'UPDATE',
338
+ serviceOrderKind: 'PROVIDE',
339
+ supplierCodes: ['NETOMNIA_REMEDIATION_WORK_ORDER_RESCHEDULED'],
340
+ yfEffect: {
341
+ kind: 'WO_SCHEDULE_APPOINTMENT',
342
+ scheduleAppointment: {
343
+ dateFrom: 'appointmentDate',
344
+ timeBlockFrom: 'appointmentTimeBlock',
345
+ supplierReferenceFrom: 'provideServiceOrder.appointmentReservationReference',
346
+ rescheduleReason: 'SCHEDULE_CHANGED',
347
+ },
348
+ notes: 'Supplier-confirmed REMEDIATION reschedule. Update the WO ' +
349
+ 'ServiceAppointment Date/TimeBlock in place, land the WO in Scheduled, ' +
350
+ 'and record the SA_RESCHEDULE ChangeReason. No Order stage / CommittedDate ' +
351
+ 'effect (REMEDIATION is its own WO; parent INSTALL stage unchanged).',
352
+ },
353
+ logAgainstOrder: true,
354
+ },
324
355
  // ═══════════════════════════════════════════════════════════════════
325
356
  // REPAIR — SERVICE WO state changes (post-install fault / service visit)
326
357
  //
@@ -365,6 +396,30 @@ exports.KCI_EFFECT_ROWS = [
365
396
  },
366
397
  logAgainstOrder: true,
367
398
  },
399
+ {
400
+ // Supplier rescheduled the SERVICE (repair) visit. Move the SERVICE WO's
401
+ // appointment to the new slot and land it back in Scheduled (from any
402
+ // non-terminal stage, incl. EnRoute/InProgress), recording an
403
+ // SA_RESCHEDULE ChangeReason. Detected REPAIR via the
404
+ // NETOMNIA_SERVICE_WORK_ORDER_ prefix in detectKciKind.
405
+ id: 'repair-update-service-rescheduled',
406
+ reasonCode: 'UPDATE',
407
+ serviceOrderKind: 'REPAIR',
408
+ supplierCodes: ['NETOMNIA_SERVICE_WORK_ORDER_RESCHEDULED'],
409
+ yfEffect: {
410
+ kind: 'WO_SCHEDULE_APPOINTMENT',
411
+ scheduleAppointment: {
412
+ dateFrom: 'appointmentDate',
413
+ timeBlockFrom: 'appointmentTimeBlock',
414
+ supplierReferenceFrom: 'provideServiceOrder.appointmentReservationReference',
415
+ rescheduleReason: 'SCHEDULE_CHANGED',
416
+ },
417
+ notes: 'Supplier-confirmed SERVICE reschedule. Update the WO ServiceAppointment ' +
418
+ 'Date/TimeBlock in place, land the WO in Scheduled, and record the ' +
419
+ 'SA_RESCHEDULE ChangeReason. No Order stage effect (changesOrderStage:false).',
420
+ },
421
+ logAgainstOrder: true,
422
+ },
368
423
  // ── PROVIDE — informational / no-action ──────────────────────────
369
424
  // Generic UPDATE row — fallback for KCIs that don't carry a recognized
370
425
  // supplierCode. Walker matches this only after rejecting all
@@ -543,13 +598,15 @@ function decideKciDispatch(dto) {
543
598
  if (!row.yfEffect.scheduleAppointment) {
544
599
  throw new Error(`bridge invariant: WO_SCHEDULE_APPOINTMENT row ${row.id} missing scheduleAppointment`);
545
600
  }
546
- return Object.assign({ kind: 'WO_SCHEDULE_APPOINTMENT', row, dateFrom: row.yfEffect.scheduleAppointment.dateFrom, timeBlockFrom: row.yfEffect.scheduleAppointment.timeBlockFrom, supplierReferenceFrom: row.yfEffect.scheduleAppointment.supplierReferenceFrom }, (row.yfEffect.propertyWrite
601
+ return Object.assign(Object.assign({ kind: 'WO_SCHEDULE_APPOINTMENT', row, dateFrom: row.yfEffect.scheduleAppointment.dateFrom, timeBlockFrom: row.yfEffect.scheduleAppointment.timeBlockFrom, supplierReferenceFrom: row.yfEffect.scheduleAppointment.supplierReferenceFrom }, (row.yfEffect.propertyWrite
547
602
  ? {
548
603
  propertyWrite: {
549
604
  entityKey: row.yfEffect.propertyWrite.entityKey,
550
605
  properties: row.yfEffect.propertyWrite.properties,
551
606
  },
552
607
  }
608
+ : {})), (row.yfEffect.scheduleAppointment.rescheduleReason
609
+ ? { rescheduleReason: row.yfEffect.scheduleAppointment.rescheduleReason }
553
610
  : {}));
554
611
  case 'PROPERTY_WRITE':
555
612
  if (!row.yfEffect.propertyWrite) {
@@ -140,10 +140,39 @@ function run() {
140
140
  const decision = (0, kci_effect_bridge_1.decideKciDispatch)(makeProvideDto('RESUMED'));
141
141
  assert('kind=LOG_KCI_ONLY', decision.kind === 'LOG_KCI_ONLY');
142
142
  }
143
- console.log('Test: REAPPOINTED → WO_SCHEDULE_APPOINTMENT (bundled stage move + appointment write)');
143
+ console.log('Test: REAPPOINTED → WO_SCHEDULE_APPOINTMENT (reschedule: flat fields + rescheduleReason)');
144
144
  {
145
145
  const decision = (0, kci_effect_bridge_1.decideKciDispatch)(makeProvideDto('REAPPOINTED'));
146
146
  assert('kind=WO_SCHEDULE_APPOINTMENT', decision.kind === 'WO_SCHEDULE_APPOINTMENT');
147
+ if (decision.kind === 'WO_SCHEDULE_APPOINTMENT') {
148
+ assert('dateFrom=appointmentDate', decision.dateFrom === 'appointmentDate');
149
+ assert('timeBlockFrom=appointmentTimeBlock', decision.timeBlockFrom === 'appointmentTimeBlock');
150
+ assert('rescheduleReason=SCHEDULE_CHANGED', decision.rescheduleReason === 'SCHEDULE_CHANGED');
151
+ }
152
+ }
153
+ console.log('Test: SERVICE WORK_ORDER_RESCHEDULED → WO_SCHEDULE_APPOINTMENT (REPAIR reschedule)');
154
+ {
155
+ const dto = makeProvideDto('UPDATE');
156
+ dto.supplierCodes = ['NETOMNIA_SERVICE_WORK_ORDER_RESCHEDULED'];
157
+ const decision = (0, kci_effect_bridge_1.decideKciDispatch)(dto);
158
+ assert('detectKciKind=REPAIR', (0, kci_effect_bridge_1.detectKciKind)(dto) === 'REPAIR');
159
+ assert('kind=WO_SCHEDULE_APPOINTMENT', decision.kind === 'WO_SCHEDULE_APPOINTMENT');
160
+ if (decision.kind === 'WO_SCHEDULE_APPOINTMENT') {
161
+ assert('row=repair-update-service-rescheduled', decision.row.id === 'repair-update-service-rescheduled');
162
+ assert('rescheduleReason=SCHEDULE_CHANGED', decision.rescheduleReason === 'SCHEDULE_CHANGED');
163
+ }
164
+ }
165
+ console.log('Test: REMEDIATION WORK_ORDER_RESCHEDULED → WO_SCHEDULE_APPOINTMENT (PROVIDE reschedule)');
166
+ {
167
+ const dto = makeProvideDto('UPDATE');
168
+ dto.supplierCodes = ['NETOMNIA_REMEDIATION_WORK_ORDER_RESCHEDULED'];
169
+ const decision = (0, kci_effect_bridge_1.decideKciDispatch)(dto);
170
+ assert('detectKciKind=PROVIDE', (0, kci_effect_bridge_1.detectKciKind)(dto) === 'PROVIDE');
171
+ assert('kind=WO_SCHEDULE_APPOINTMENT', decision.kind === 'WO_SCHEDULE_APPOINTMENT');
172
+ if (decision.kind === 'WO_SCHEDULE_APPOINTMENT') {
173
+ assert('row=provide-update-remediation-rescheduled', decision.row.id === 'provide-update-remediation-rescheduled');
174
+ assert('rescheduleReason=SCHEDULE_CHANGED', decision.rescheduleReason === 'SCHEDULE_CHANGED');
175
+ }
147
176
  }
148
177
  console.log('Test: WARNING → LOG_KCI_ONLY');
149
178
  {
@@ -496,6 +496,15 @@ export interface FibreCafeKciDto {
496
496
  * supplierCode discriminator needed).
497
497
  */
498
498
  tenantWorkOrderReference?: string | null;
499
+ /**
500
+ * Rebooked appointment slot — set on REAPPOINTED and the SERVICE /
501
+ * REMEDIATION *_RESCHEDULED UPDATE KCIs. NET stamps these flat envelope
502
+ * fields directly (in preference to the nested
503
+ * provideServiceOrder.appointmentTimeslot). `appointmentDate` is the ISO
504
+ * appointment date; `appointmentTimeBlock` is the AM/PM block.
505
+ */
506
+ appointmentDate?: string;
507
+ appointmentTimeBlock?: 'AM' | 'PM';
499
508
  /** Full Odin WorkOrder record — sent for WO stage/appointment updates. */
500
509
  workOrder?: OdinRecord<WorkOrderProperties>;
501
510
  /** Partial Odin ServiceAppointment record — sent for WO stage/appointment updates. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@d19n/youfibre-odin-sdk",
3
- "version": "2.0.157",
3
+ "version": "2.0.158",
4
4
  "description": "",
5
5
  "author": "@d19n",
6
6
  "license": "UNLICENSED",