@d19n/youfibre-odin-sdk 2.0.103-beta.13 → 2.0.103-beta.14
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/wholesale-provider/index.d.ts +2 -0
- package/dist/wholesale-provider/index.js +6 -1
- package/dist/wholesale-provider/kci-effect-bridge.d.ts +94 -0
- package/dist/wholesale-provider/kci-effect-bridge.js +325 -0
- package/dist/wholesale-provider/test/kci-effect-bridge.spec.d.ts +1 -0
- package/dist/wholesale-provider/test/kci-effect-bridge.spec.js +211 -0
- package/package.json +2 -2
|
@@ -29,5 +29,7 @@ export type { WholesaleProvider, CreateOrderOptions, CancellationReasonCode, Res
|
|
|
29
29
|
export type { WholesaleResult, WholesaleSyncResponse, WholesaleRejectionResponse, WholesaleRejectionMessage, WholesaleBUS, WholesaleSYS, ISO8601DateTime, ISODate, TransactionId, UPRN, UUID, SwitchOrderReference, } from './types/wholesale';
|
|
30
30
|
export { NetomniaProvider, AggregateNotificationError } from './providers/netomnia-provider';
|
|
31
31
|
export type { NetomniaProviderDeps } from './providers/netomnia-provider';
|
|
32
|
+
export { KCI_EFFECT_ROWS, decideKciDispatch, detectKciKind, } from './kci-effect-bridge';
|
|
33
|
+
export type { KciServiceOrderKind, YfWoComposedActionKey, YfKciEffectKind, YfKciTargetEntity, YfKciEffect, KciEffectRow, KciDispatchDecision, } from './kci-effect-bridge';
|
|
32
34
|
export type { VMO2OrderId, VMO2RequestId, VMO2AppointmentKey, VMO2PremiseId, VMO2OrderNotificationType, VMO2SMRNotificationType, VMO2NotificationEnvelope, VMO2OrderNotification, VMO2SMRNotification, VMO2InboundNotification, } from './types/vmo2';
|
|
33
35
|
export * from './types/fibrecafe';
|
|
@@ -41,11 +41,16 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
41
41
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
42
42
|
};
|
|
43
43
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
-
exports.AggregateNotificationError = exports.NetomniaProvider = void 0;
|
|
44
|
+
exports.detectKciKind = exports.decideKciDispatch = exports.KCI_EFFECT_ROWS = exports.AggregateNotificationError = exports.NetomniaProvider = void 0;
|
|
45
45
|
// ── Provider implementation ───────────────────────────────────────────
|
|
46
46
|
var netomnia_provider_1 = require("./providers/netomnia-provider");
|
|
47
47
|
Object.defineProperty(exports, "NetomniaProvider", { enumerable: true, get: function () { return netomnia_provider_1.NetomniaProvider; } });
|
|
48
48
|
Object.defineProperty(exports, "AggregateNotificationError", { enumerable: true, get: function () { return netomnia_provider_1.AggregateNotificationError; } });
|
|
49
|
+
// ── KCI effect bridge (canonical typed catalog + walker) ──────────────
|
|
50
|
+
var kci_effect_bridge_1 = require("./kci-effect-bridge");
|
|
51
|
+
Object.defineProperty(exports, "KCI_EFFECT_ROWS", { enumerable: true, get: function () { return kci_effect_bridge_1.KCI_EFFECT_ROWS; } });
|
|
52
|
+
Object.defineProperty(exports, "decideKciDispatch", { enumerable: true, get: function () { return kci_effect_bridge_1.decideKciDispatch; } });
|
|
53
|
+
Object.defineProperty(exports, "detectKciKind", { enumerable: true, get: function () { return kci_effect_bridge_1.detectKciKind; } });
|
|
49
54
|
// ── Reference types (re-exported — not on WholesaleProvider surface) ──
|
|
50
55
|
//
|
|
51
56
|
// FibreCafe wire-format types — useful for module-level DTO consolidation
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KCI → YF effect bridge — canonical typed catalog of how every supplier
|
|
3
|
+
* KCI maps to a YF-side action.
|
|
4
|
+
*
|
|
5
|
+
* **Contract:**
|
|
6
|
+
*
|
|
7
|
+
* 1. Cannot cancel an INSTALL WorkOrder directly — must cancel the Order.
|
|
8
|
+
* 2. Cannot move a WO to any stage directly — only via composed actions.
|
|
9
|
+
* 3. KCIs from providers ONLY move the INSTALL WO. Never the Order.
|
|
10
|
+
* 4. Every KCI is logged against the Order as an OrderKci record.
|
|
11
|
+
*
|
|
12
|
+
* The type system encodes rules #1-3 structurally. `YfKciEffectKind` does
|
|
13
|
+
* NOT include `ORDER_TRANSITION` (rule #3), `WO_CANCEL` or `WO_TRANSITION`
|
|
14
|
+
* (rules #1, #2). Only `WO_FLOW_ACTION`, `PROPERTY_WRITE`, `LOG_KCI_ONLY`,
|
|
15
|
+
* or `NO_OP_DOCUMENTED` reach the WO. Action keys are constrained to the
|
|
16
|
+
* `YfWoComposedActionKey` union — every value grep-resolves in
|
|
17
|
+
* `packages/youfibre-odin-sdk/src/actions-v2/<Name>Dto.ts`.
|
|
18
|
+
*
|
|
19
|
+
* Companion: `agent-os/knowledge/business-processes/yf-kci-effect-bridge.ts`
|
|
20
|
+
* — same content as documentation; the SDK file here is the executable
|
|
21
|
+
* source of truth that production consumers (`receiveKci` walker) import.
|
|
22
|
+
*
|
|
23
|
+
* Linked: T-20260508-010 Step 3 followup (receiveKci 100% typed + handled).
|
|
24
|
+
*/
|
|
25
|
+
import type { FibreCafeKciDto, FibreCafeKciReasonCode } from './types/fibrecafe';
|
|
26
|
+
export declare type KciServiceOrderKind = 'PROVIDE' | 'CEASE' | 'MODIFY' | 'UNSOLICITED_CEASE';
|
|
27
|
+
/**
|
|
28
|
+
* Allowed YF SDK composed action keys. Every value MUST grep-resolve as
|
|
29
|
+
* `actionKey` in `packages/youfibre-odin-sdk/src/actions-v2/<Name>Dto.ts`.
|
|
30
|
+
*/
|
|
31
|
+
export declare type YfWoComposedActionKey = 'MoveInstallWorkOrderToScheduled' | 'MoveInstallWorkOrderFromInProgressToPending' | 'MoveInstallWorkOrderFromPendingToInProgress' | 'MoveInstallWorkOrderToRemediation' | 'MoveInstallWorkOrderFromRemediationToInProgress' | 'CreateInstallRescheduledChangeReasonSupplier' | 'CompleteInstallWorkOrderFlow' | 'CancelInstallWorkOrderFlowSupplier' | 'CreateInstallCancelledChangeReasonSupplier';
|
|
32
|
+
export declare type YfKciEffectKind = 'LOG_KCI_ONLY' | 'WO_FLOW_ACTION' | 'PROPERTY_WRITE' | 'NO_OP_DOCUMENTED';
|
|
33
|
+
export declare type YfKciTargetEntity = 'OrderModule:Order' | 'CrmModule:Address' | 'FieldServiceModule:WorkOrder';
|
|
34
|
+
export interface YfKciEffect {
|
|
35
|
+
readonly kind: YfKciEffectKind;
|
|
36
|
+
/** For WO_FLOW_ACTION: the composed action key. */
|
|
37
|
+
readonly woActionKey?: YfWoComposedActionKey;
|
|
38
|
+
/** For PROPERTY_WRITE: target entity + property keys. */
|
|
39
|
+
readonly propertyWrite?: {
|
|
40
|
+
readonly entityKey: YfKciTargetEntity;
|
|
41
|
+
readonly properties: ReadonlyArray<string>;
|
|
42
|
+
};
|
|
43
|
+
/** For NO_OP_DOCUMENTED: required reason text. */
|
|
44
|
+
readonly noOpReason?: string;
|
|
45
|
+
readonly notes: string;
|
|
46
|
+
}
|
|
47
|
+
export interface KciEffectRow {
|
|
48
|
+
readonly id: string;
|
|
49
|
+
readonly reasonCode: FibreCafeKciReasonCode;
|
|
50
|
+
readonly serviceOrderKind: KciServiceOrderKind;
|
|
51
|
+
readonly yfOrderStagePreconditions?: ReadonlyArray<string>;
|
|
52
|
+
readonly yfEffect: YfKciEffect;
|
|
53
|
+
/** Always true per rule #4. Field is explicit so future rows that
|
|
54
|
+
* document an intentional skip can mark it false. */
|
|
55
|
+
readonly logAgainstOrder: boolean;
|
|
56
|
+
readonly specRef?: string;
|
|
57
|
+
}
|
|
58
|
+
export declare const KCI_EFFECT_ROWS: ReadonlyArray<KciEffectRow>;
|
|
59
|
+
export declare type KciDispatchDecision = {
|
|
60
|
+
kind: 'LOG_KCI_ONLY';
|
|
61
|
+
row: KciEffectRow;
|
|
62
|
+
} | {
|
|
63
|
+
kind: 'WO_FLOW_ACTION';
|
|
64
|
+
row: KciEffectRow;
|
|
65
|
+
woActionKey: YfWoComposedActionKey;
|
|
66
|
+
} | {
|
|
67
|
+
kind: 'PROPERTY_WRITE';
|
|
68
|
+
row: KciEffectRow;
|
|
69
|
+
entityKey: YfKciTargetEntity;
|
|
70
|
+
properties: ReadonlyArray<string>;
|
|
71
|
+
} | {
|
|
72
|
+
kind: 'NO_OP_DOCUMENTED';
|
|
73
|
+
row: KciEffectRow;
|
|
74
|
+
reason: string;
|
|
75
|
+
} | {
|
|
76
|
+
kind: 'NO_MATCH';
|
|
77
|
+
reasonCode: FibreCafeKciReasonCode;
|
|
78
|
+
serviceOrderKind: KciServiceOrderKind;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Determine which serviceOrder kind the KCI is about by inspecting which
|
|
82
|
+
* sub-body field is populated.
|
|
83
|
+
*/
|
|
84
|
+
export declare function detectKciKind(dto: FibreCafeKciDto): KciServiceOrderKind | null;
|
|
85
|
+
/**
|
|
86
|
+
* Pure function — given a KCI dto, return the dispatch decision per the
|
|
87
|
+
* bridge. Caller (`receiveKci` in netomnia.provider.ts) executes the
|
|
88
|
+
* decision (fires the SDK action / writes the property / logs the KCI).
|
|
89
|
+
*
|
|
90
|
+
* The decision type is exhaustive: no `default` fall-through; missing
|
|
91
|
+
* (reasonCode × kind) tuples return `NO_MATCH` so the caller can surface
|
|
92
|
+
* the gap explicitly.
|
|
93
|
+
*/
|
|
94
|
+
export declare function decideKciDispatch(dto: FibreCafeKciDto): KciDispatchDecision;
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.decideKciDispatch = exports.detectKciKind = exports.KCI_EFFECT_ROWS = void 0;
|
|
4
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
5
|
+
// Bridge rows — PROVIDE coverage (24/24)
|
|
6
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
7
|
+
//
|
|
8
|
+
// CEASE / MODIFY / UNSOLICITED_CEASE are out of scope per Frank
|
|
9
|
+
// 2026-05-09. Add rows when those journeys are tested.
|
|
10
|
+
exports.KCI_EFFECT_ROWS = [
|
|
11
|
+
// ── PROVIDE — happy path ──────────────────────────────────────────
|
|
12
|
+
{
|
|
13
|
+
id: 'provide-acknowledged',
|
|
14
|
+
reasonCode: 'ACKNOWLEDGED',
|
|
15
|
+
serviceOrderKind: 'PROVIDE',
|
|
16
|
+
yfEffect: {
|
|
17
|
+
kind: 'PROPERTY_WRITE',
|
|
18
|
+
propertyWrite: { entityKey: 'OrderModule:Order', properties: ['ExternalReference'] },
|
|
19
|
+
notes: 'Stamp NET supplierOrderReference onto YF Order.ExternalReference.',
|
|
20
|
+
},
|
|
21
|
+
logAgainstOrder: true,
|
|
22
|
+
specRef: 'fibrecafe-docs/pages/orderstate.md (ACKNOWLEDGED)',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'provide-committed',
|
|
26
|
+
reasonCode: 'COMMITTED',
|
|
27
|
+
serviceOrderKind: 'PROVIDE',
|
|
28
|
+
yfEffect: {
|
|
29
|
+
kind: 'PROPERTY_WRITE',
|
|
30
|
+
propertyWrite: { entityKey: 'OrderModule:Order', properties: ['CommittedDate'] },
|
|
31
|
+
notes: 'Persist supplier-committed fulfillment date. Order stage already moved Sold→Supply on dispatch (rule #3: KCI does not move Order).',
|
|
32
|
+
},
|
|
33
|
+
logAgainstOrder: true,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'provide-completed',
|
|
37
|
+
reasonCode: 'COMPLETED',
|
|
38
|
+
serviceOrderKind: 'PROVIDE',
|
|
39
|
+
yfEffect: {
|
|
40
|
+
kind: 'WO_FLOW_ACTION',
|
|
41
|
+
woActionKey: 'CompleteInstallWorkOrderFlow',
|
|
42
|
+
notes: 'Supplier confirmed install completion. Composed flow handles WO Done + downstream (ISP reprovision, etc.). Order Sold→Supply→Active cascade is downstream of WO completion.',
|
|
43
|
+
},
|
|
44
|
+
logAgainstOrder: true,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'provide-cancelled',
|
|
48
|
+
reasonCode: 'CANCELLED',
|
|
49
|
+
serviceOrderKind: 'PROVIDE',
|
|
50
|
+
yfEffect: {
|
|
51
|
+
kind: 'WO_FLOW_ACTION',
|
|
52
|
+
woActionKey: 'CancelInstallWorkOrderFlowSupplier',
|
|
53
|
+
notes: 'Supplier-initiated cancel. Composed flow handles WO transition + Order cascade — NOT a raw stage move (rule #1).',
|
|
54
|
+
},
|
|
55
|
+
logAgainstOrder: true,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'provide-failed',
|
|
59
|
+
reasonCode: 'FAILED',
|
|
60
|
+
serviceOrderKind: 'PROVIDE',
|
|
61
|
+
yfEffect: {
|
|
62
|
+
kind: 'WO_FLOW_ACTION',
|
|
63
|
+
woActionKey: 'CancelInstallWorkOrderFlowSupplier',
|
|
64
|
+
notes: 'Same disposition as CANCELLED — install will not happen.',
|
|
65
|
+
},
|
|
66
|
+
logAgainstOrder: true,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'provide-rejected',
|
|
70
|
+
reasonCode: 'REJECTED',
|
|
71
|
+
serviceOrderKind: 'PROVIDE',
|
|
72
|
+
yfEffect: {
|
|
73
|
+
kind: 'WO_FLOW_ACTION',
|
|
74
|
+
woActionKey: 'CancelInstallWorkOrderFlowSupplier',
|
|
75
|
+
notes: 'Order rejected at validation time. Supplier-cancel flow.',
|
|
76
|
+
},
|
|
77
|
+
logAgainstOrder: true,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'provide-order-rejected',
|
|
81
|
+
reasonCode: 'ORDER_REJECTED',
|
|
82
|
+
serviceOrderKind: 'PROVIDE',
|
|
83
|
+
yfEffect: {
|
|
84
|
+
kind: 'WO_FLOW_ACTION',
|
|
85
|
+
woActionKey: 'CancelInstallWorkOrderFlowSupplier',
|
|
86
|
+
notes: 'Order rejected at processing.',
|
|
87
|
+
},
|
|
88
|
+
logAgainstOrder: true,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'provide-partial',
|
|
92
|
+
reasonCode: 'PARTIAL',
|
|
93
|
+
serviceOrderKind: 'PROVIDE',
|
|
94
|
+
yfEffect: {
|
|
95
|
+
kind: 'WO_FLOW_ACTION',
|
|
96
|
+
woActionKey: 'CompleteInstallWorkOrderFlow',
|
|
97
|
+
notes: 'Partial install — primary tasks done, side tasks deferred. Treated as terminal complete on YF side.',
|
|
98
|
+
},
|
|
99
|
+
logAgainstOrder: true,
|
|
100
|
+
},
|
|
101
|
+
// ── PROVIDE — Pending (tenant action required) ───────────────────
|
|
102
|
+
{
|
|
103
|
+
id: 'provide-information-required',
|
|
104
|
+
reasonCode: 'INFORMATION_REQUIRED',
|
|
105
|
+
serviceOrderKind: 'PROVIDE',
|
|
106
|
+
yfEffect: {
|
|
107
|
+
kind: 'WO_FLOW_ACTION',
|
|
108
|
+
woActionKey: 'MoveInstallWorkOrderFromInProgressToPending',
|
|
109
|
+
notes: 'Supplier needs additional info before continuing. WO → Pending.',
|
|
110
|
+
},
|
|
111
|
+
logAgainstOrder: true,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'provide-contact-customer',
|
|
115
|
+
reasonCode: 'CONTACT_CUSTOMER',
|
|
116
|
+
serviceOrderKind: 'PROVIDE',
|
|
117
|
+
yfEffect: {
|
|
118
|
+
kind: 'WO_FLOW_ACTION',
|
|
119
|
+
woActionKey: 'MoveInstallWorkOrderFromInProgressToPending',
|
|
120
|
+
notes: 'Supplier asks YF to contact customer. WO → Pending.',
|
|
121
|
+
},
|
|
122
|
+
logAgainstOrder: true,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: 'provide-contact-reappoint',
|
|
126
|
+
reasonCode: 'CONTACT_REAPPOINT',
|
|
127
|
+
serviceOrderKind: 'PROVIDE',
|
|
128
|
+
yfEffect: {
|
|
129
|
+
kind: 'WO_FLOW_ACTION',
|
|
130
|
+
woActionKey: 'MoveInstallWorkOrderFromInProgressToPending',
|
|
131
|
+
notes: 'Customer-initiated reappointment needed. WO → Pending until new appointment.',
|
|
132
|
+
},
|
|
133
|
+
logAgainstOrder: true,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: 'provide-reappoint',
|
|
137
|
+
reasonCode: 'REAPPOINT',
|
|
138
|
+
serviceOrderKind: 'PROVIDE',
|
|
139
|
+
yfEffect: {
|
|
140
|
+
kind: 'WO_FLOW_ACTION',
|
|
141
|
+
woActionKey: 'MoveInstallWorkOrderFromInProgressToPending',
|
|
142
|
+
notes: 'Supplier requests reappointment. WO → Pending. Followed by REAPPOINTED when slot reserved.',
|
|
143
|
+
},
|
|
144
|
+
logAgainstOrder: true,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'provide-resumed',
|
|
148
|
+
reasonCode: 'RESUMED',
|
|
149
|
+
serviceOrderKind: 'PROVIDE',
|
|
150
|
+
yfEffect: {
|
|
151
|
+
kind: 'WO_FLOW_ACTION',
|
|
152
|
+
woActionKey: 'MoveInstallWorkOrderFromPendingToInProgress',
|
|
153
|
+
notes: 'Supplier resumed fulfillment after Pending/Held.',
|
|
154
|
+
},
|
|
155
|
+
logAgainstOrder: true,
|
|
156
|
+
},
|
|
157
|
+
// ── PROVIDE — Remediation ────────────────────────────────────────
|
|
158
|
+
{
|
|
159
|
+
id: 'provide-delay',
|
|
160
|
+
reasonCode: 'DELAY',
|
|
161
|
+
serviceOrderKind: 'PROVIDE',
|
|
162
|
+
yfEffect: {
|
|
163
|
+
kind: 'WO_FLOW_ACTION',
|
|
164
|
+
woActionKey: 'MoveInstallWorkOrderToRemediation',
|
|
165
|
+
notes: "Supplier delay — install can't complete on planned date. WO → RemediationRequired for triage.",
|
|
166
|
+
},
|
|
167
|
+
logAgainstOrder: true,
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: 'provide-resolved',
|
|
171
|
+
reasonCode: 'RESOLVED',
|
|
172
|
+
serviceOrderKind: 'PROVIDE',
|
|
173
|
+
yfEffect: {
|
|
174
|
+
kind: 'WO_FLOW_ACTION',
|
|
175
|
+
woActionKey: 'MoveInstallWorkOrderFromRemediationToInProgress',
|
|
176
|
+
notes: 'Problem resolved without target-date impact. WO → InProgress.',
|
|
177
|
+
},
|
|
178
|
+
logAgainstOrder: true,
|
|
179
|
+
},
|
|
180
|
+
// ── PROVIDE — appointment confirmation ────────────────────────────
|
|
181
|
+
{
|
|
182
|
+
id: 'provide-reappointed',
|
|
183
|
+
reasonCode: 'REAPPOINTED',
|
|
184
|
+
serviceOrderKind: 'PROVIDE',
|
|
185
|
+
yfEffect: {
|
|
186
|
+
kind: 'WO_FLOW_ACTION',
|
|
187
|
+
woActionKey: 'CreateInstallRescheduledChangeReasonSupplier',
|
|
188
|
+
notes: 'Supplier-side reappointment confirmed. Reschedule flow + ChangeReason for audit.',
|
|
189
|
+
},
|
|
190
|
+
logAgainstOrder: true,
|
|
191
|
+
},
|
|
192
|
+
// ── PROVIDE — informational / no-action ──────────────────────────
|
|
193
|
+
{
|
|
194
|
+
id: 'provide-update',
|
|
195
|
+
reasonCode: 'UPDATE',
|
|
196
|
+
serviceOrderKind: 'PROVIDE',
|
|
197
|
+
yfEffect: { kind: 'LOG_KCI_ONLY', notes: 'General update; no specific action.' },
|
|
198
|
+
logAgainstOrder: true,
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: 'provide-warning',
|
|
202
|
+
reasonCode: 'WARNING',
|
|
203
|
+
serviceOrderKind: 'PROVIDE',
|
|
204
|
+
yfEffect: { kind: 'LOG_KCI_ONLY', notes: 'Supplier warning, informational.' },
|
|
205
|
+
logAgainstOrder: true,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: 'provide-additional',
|
|
209
|
+
reasonCode: 'ADDITIONAL',
|
|
210
|
+
serviceOrderKind: 'PROVIDE',
|
|
211
|
+
yfEffect: { kind: 'LOG_KCI_ONLY', notes: 'Additional info added (notes / engineer tasks).' },
|
|
212
|
+
logAgainstOrder: true,
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: 'provide-created',
|
|
216
|
+
reasonCode: 'CREATED',
|
|
217
|
+
serviceOrderKind: 'PROVIDE',
|
|
218
|
+
yfEffect: { kind: 'LOG_KCI_ONLY', notes: 'Rare on PROVIDE; supplier-created entity.' },
|
|
219
|
+
logAgainstOrder: true,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: 'provide-resubmit',
|
|
223
|
+
reasonCode: 'RESUBMIT',
|
|
224
|
+
serviceOrderKind: 'PROVIDE',
|
|
225
|
+
yfEffect: {
|
|
226
|
+
kind: 'LOG_KCI_ONLY',
|
|
227
|
+
notes: 'Supplier asks tenant to resubmit. Manual ops triage.',
|
|
228
|
+
},
|
|
229
|
+
logAgainstOrder: true,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: 'provide-amended',
|
|
233
|
+
reasonCode: 'AMENDED',
|
|
234
|
+
serviceOrderKind: 'PROVIDE',
|
|
235
|
+
yfEffect: {
|
|
236
|
+
kind: 'LOG_KCI_ONLY',
|
|
237
|
+
notes: 'Amendment applied at supplier side. WO continues at current stage.',
|
|
238
|
+
},
|
|
239
|
+
logAgainstOrder: true,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
id: 'provide-amendment-rejected',
|
|
243
|
+
reasonCode: 'AMENDMENT_REJECTED',
|
|
244
|
+
serviceOrderKind: 'PROVIDE',
|
|
245
|
+
yfEffect: {
|
|
246
|
+
kind: 'LOG_KCI_ONLY',
|
|
247
|
+
notes: 'Supplier rejected an amendment request. WO continues with original spec.',
|
|
248
|
+
},
|
|
249
|
+
logAgainstOrder: true,
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
id: 'provide-cancellation-rejected',
|
|
253
|
+
reasonCode: 'CANCELLATION_REJECTED',
|
|
254
|
+
serviceOrderKind: 'PROVIDE',
|
|
255
|
+
yfEffect: {
|
|
256
|
+
kind: 'LOG_KCI_ONLY',
|
|
257
|
+
notes: 'Supplier rejected cancellation request. WO continues; ops triage.',
|
|
258
|
+
},
|
|
259
|
+
logAgainstOrder: true,
|
|
260
|
+
},
|
|
261
|
+
];
|
|
262
|
+
/**
|
|
263
|
+
* Determine which serviceOrder kind the KCI is about by inspecting which
|
|
264
|
+
* sub-body field is populated.
|
|
265
|
+
*/
|
|
266
|
+
function detectKciKind(dto) {
|
|
267
|
+
if (dto.provideServiceOrder)
|
|
268
|
+
return 'PROVIDE';
|
|
269
|
+
if (dto.ceaseServiceOrder)
|
|
270
|
+
return 'CEASE';
|
|
271
|
+
if (dto.modifyServiceOrder)
|
|
272
|
+
return 'MODIFY';
|
|
273
|
+
if (dto.unsolicitedCeaseServiceOrder)
|
|
274
|
+
return 'UNSOLICITED_CEASE';
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
exports.detectKciKind = detectKciKind;
|
|
278
|
+
/**
|
|
279
|
+
* Pure function — given a KCI dto, return the dispatch decision per the
|
|
280
|
+
* bridge. Caller (`receiveKci` in netomnia.provider.ts) executes the
|
|
281
|
+
* decision (fires the SDK action / writes the property / logs the KCI).
|
|
282
|
+
*
|
|
283
|
+
* The decision type is exhaustive: no `default` fall-through; missing
|
|
284
|
+
* (reasonCode × kind) tuples return `NO_MATCH` so the caller can surface
|
|
285
|
+
* the gap explicitly.
|
|
286
|
+
*/
|
|
287
|
+
function decideKciDispatch(dto) {
|
|
288
|
+
const kind = detectKciKind(dto);
|
|
289
|
+
if (!kind) {
|
|
290
|
+
return {
|
|
291
|
+
kind: 'NO_MATCH',
|
|
292
|
+
reasonCode: dto.reasonCode,
|
|
293
|
+
serviceOrderKind: 'PROVIDE',
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const row = exports.KCI_EFFECT_ROWS.find((r) => r.reasonCode === dto.reasonCode && r.serviceOrderKind === kind);
|
|
297
|
+
if (!row) {
|
|
298
|
+
return { kind: 'NO_MATCH', reasonCode: dto.reasonCode, serviceOrderKind: kind };
|
|
299
|
+
}
|
|
300
|
+
switch (row.yfEffect.kind) {
|
|
301
|
+
case 'LOG_KCI_ONLY':
|
|
302
|
+
return { kind: 'LOG_KCI_ONLY', row };
|
|
303
|
+
case 'WO_FLOW_ACTION':
|
|
304
|
+
if (!row.yfEffect.woActionKey) {
|
|
305
|
+
throw new Error(`bridge invariant: WO_FLOW_ACTION row ${row.id} missing woActionKey`);
|
|
306
|
+
}
|
|
307
|
+
return { kind: 'WO_FLOW_ACTION', row, woActionKey: row.yfEffect.woActionKey };
|
|
308
|
+
case 'PROPERTY_WRITE':
|
|
309
|
+
if (!row.yfEffect.propertyWrite) {
|
|
310
|
+
throw new Error(`bridge invariant: PROPERTY_WRITE row ${row.id} missing propertyWrite`);
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
kind: 'PROPERTY_WRITE',
|
|
314
|
+
row,
|
|
315
|
+
entityKey: row.yfEffect.propertyWrite.entityKey,
|
|
316
|
+
properties: row.yfEffect.propertyWrite.properties,
|
|
317
|
+
};
|
|
318
|
+
case 'NO_OP_DOCUMENTED':
|
|
319
|
+
if (!row.yfEffect.noOpReason) {
|
|
320
|
+
throw new Error(`bridge invariant: NO_OP_DOCUMENTED row ${row.id} missing noOpReason`);
|
|
321
|
+
}
|
|
322
|
+
return { kind: 'NO_OP_DOCUMENTED', row, reason: row.yfEffect.noOpReason };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
exports.decideKciDispatch = decideKciDispatch;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* KCI effect bridge walker tests.
|
|
5
|
+
*
|
|
6
|
+
* Verifies:
|
|
7
|
+
* 1. detectKciKind correctly classifies dto by populated sub-body
|
|
8
|
+
* 2. decideKciDispatch returns the correct decision for every (reasonCode × kind)
|
|
9
|
+
* tuple in the bridge
|
|
10
|
+
* 3. Coverage: every PROVIDE × KciReasonCode tuple has a row (no NO_MATCH for in-scope)
|
|
11
|
+
* 4. Bridge invariants hold (every WO_FLOW_ACTION row has woActionKey, etc.)
|
|
12
|
+
*
|
|
13
|
+
* Run: npm run test:wholesale-provider — runs all wholesale provider specs
|
|
14
|
+
*/
|
|
15
|
+
const kci_effect_bridge_1 = require("../kci-effect-bridge");
|
|
16
|
+
let passed = 0;
|
|
17
|
+
let failed = 0;
|
|
18
|
+
const failures = [];
|
|
19
|
+
function assert(label, cond, hint) {
|
|
20
|
+
if (cond) {
|
|
21
|
+
passed += 1;
|
|
22
|
+
console.log(` PASS ${label}`);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
failed += 1;
|
|
26
|
+
const msg = ` FAIL ${label}${hint ? ' — ' + hint : ''}`;
|
|
27
|
+
failures.push(msg);
|
|
28
|
+
console.log(msg);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function makeProvideDto(reasonCode) {
|
|
32
|
+
return {
|
|
33
|
+
id: 'kci-test',
|
|
34
|
+
sequenceNumber: 1,
|
|
35
|
+
issuedOn: '2026-05-09T00:00:00Z',
|
|
36
|
+
reasonCode,
|
|
37
|
+
provideServiceOrder: {
|
|
38
|
+
id: 'order-1',
|
|
39
|
+
tenant: 'YOUFIBRE',
|
|
40
|
+
status: 'IN_PROGRESS',
|
|
41
|
+
address: { id: '12345', type: 'UPRN' },
|
|
42
|
+
serviceOrderItem: {
|
|
43
|
+
serviceSpecification: { id: 'FTTP' },
|
|
44
|
+
serviceCharacteristics: [],
|
|
45
|
+
},
|
|
46
|
+
primaryContact: { name: 'Test', phoneNumber: '0' },
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function run() {
|
|
51
|
+
console.log('KCI effect bridge — walker tests\n');
|
|
52
|
+
// ── 1. detectKciKind correctly classifies ──────────────────────────
|
|
53
|
+
console.log('Test: detectKciKind on PROVIDE');
|
|
54
|
+
{
|
|
55
|
+
const dto = makeProvideDto('ACKNOWLEDGED');
|
|
56
|
+
assert('detectKciKind returns PROVIDE', (0, kci_effect_bridge_1.detectKciKind)(dto) === 'PROVIDE');
|
|
57
|
+
}
|
|
58
|
+
console.log('Test: detectKciKind on CEASE');
|
|
59
|
+
{
|
|
60
|
+
const dto = {
|
|
61
|
+
id: 'k',
|
|
62
|
+
sequenceNumber: 1,
|
|
63
|
+
issuedOn: '2026-05-09T00:00:00Z',
|
|
64
|
+
reasonCode: 'COMPLETED',
|
|
65
|
+
ceaseServiceOrder: { id: 'o', tenant: 'YF', status: 'COMPLETED' },
|
|
66
|
+
};
|
|
67
|
+
assert('detectKciKind returns CEASE', (0, kci_effect_bridge_1.detectKciKind)(dto) === 'CEASE');
|
|
68
|
+
}
|
|
69
|
+
console.log('Test: detectKciKind on no-body dto');
|
|
70
|
+
{
|
|
71
|
+
const dto = {
|
|
72
|
+
id: 'k',
|
|
73
|
+
sequenceNumber: 1,
|
|
74
|
+
issuedOn: '2026-05-09T00:00:00Z',
|
|
75
|
+
reasonCode: 'WARNING',
|
|
76
|
+
};
|
|
77
|
+
assert('detectKciKind returns null', (0, kci_effect_bridge_1.detectKciKind)(dto) === null);
|
|
78
|
+
}
|
|
79
|
+
// ── 2. Bridge coverage: every PROVIDE reason resolves ──────────────
|
|
80
|
+
const allReasons = [
|
|
81
|
+
'CREATED', 'ACKNOWLEDGED', 'COMMITTED', 'UPDATE',
|
|
82
|
+
'INFORMATION_REQUIRED', 'DELAY', 'RESUBMIT', 'RESOLVED', 'RESUMED',
|
|
83
|
+
'AMENDED', 'AMENDMENT_REJECTED', 'CANCELLED', 'CANCELLATION_REJECTED',
|
|
84
|
+
'COMPLETED', 'PARTIAL', 'FAILED', 'REJECTED', 'ORDER_REJECTED',
|
|
85
|
+
'REAPPOINT', 'REAPPOINTED', 'CONTACT_CUSTOMER', 'CONTACT_REAPPOINT',
|
|
86
|
+
'WARNING', 'ADDITIONAL',
|
|
87
|
+
];
|
|
88
|
+
console.log('Test: every PROVIDE reason code has a matching row (24/24)');
|
|
89
|
+
for (const reasonCode of allReasons) {
|
|
90
|
+
const dto = makeProvideDto(reasonCode);
|
|
91
|
+
const decision = (0, kci_effect_bridge_1.decideKciDispatch)(dto);
|
|
92
|
+
assert(`${reasonCode} resolves`, decision.kind !== 'NO_MATCH', decision.kind === 'NO_MATCH'
|
|
93
|
+
? `got NO_MATCH for PROVIDE × ${reasonCode}`
|
|
94
|
+
: undefined);
|
|
95
|
+
}
|
|
96
|
+
// ── 3. Specific decision shapes — spot checks per kind ─────────────
|
|
97
|
+
console.log('Test: ACKNOWLEDGED → PROPERTY_WRITE on Order.ExternalReference');
|
|
98
|
+
{
|
|
99
|
+
const decision = (0, kci_effect_bridge_1.decideKciDispatch)(makeProvideDto('ACKNOWLEDGED'));
|
|
100
|
+
assert('kind=PROPERTY_WRITE', decision.kind === 'PROPERTY_WRITE');
|
|
101
|
+
if (decision.kind === 'PROPERTY_WRITE') {
|
|
102
|
+
assert('entity=OrderModule:Order', decision.entityKey === 'OrderModule:Order');
|
|
103
|
+
assert('properties contains ExternalReference', decision.properties.includes('ExternalReference'));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
console.log('Test: COMPLETED → WO_FLOW_ACTION CompleteInstallWorkOrderFlow');
|
|
107
|
+
{
|
|
108
|
+
const decision = (0, kci_effect_bridge_1.decideKciDispatch)(makeProvideDto('COMPLETED'));
|
|
109
|
+
assert('kind=WO_FLOW_ACTION', decision.kind === 'WO_FLOW_ACTION');
|
|
110
|
+
if (decision.kind === 'WO_FLOW_ACTION') {
|
|
111
|
+
assert('woActionKey=CompleteInstallWorkOrderFlow', decision.woActionKey === 'CompleteInstallWorkOrderFlow');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
console.log('Test: CANCELLED → WO_FLOW_ACTION CancelInstallWorkOrderFlowSupplier');
|
|
115
|
+
{
|
|
116
|
+
const decision = (0, kci_effect_bridge_1.decideKciDispatch)(makeProvideDto('CANCELLED'));
|
|
117
|
+
assert('kind=WO_FLOW_ACTION', decision.kind === 'WO_FLOW_ACTION');
|
|
118
|
+
if (decision.kind === 'WO_FLOW_ACTION') {
|
|
119
|
+
assert('woActionKey=CancelInstallWorkOrderFlowSupplier', decision.woActionKey === 'CancelInstallWorkOrderFlowSupplier');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
console.log('Test: INFORMATION_REQUIRED → MoveInstallWorkOrderFromInProgressToPending');
|
|
123
|
+
{
|
|
124
|
+
const decision = (0, kci_effect_bridge_1.decideKciDispatch)(makeProvideDto('INFORMATION_REQUIRED'));
|
|
125
|
+
assert('kind=WO_FLOW_ACTION', decision.kind === 'WO_FLOW_ACTION');
|
|
126
|
+
if (decision.kind === 'WO_FLOW_ACTION') {
|
|
127
|
+
assert('woActionKey=MoveInstallWorkOrderFromInProgressToPending', decision.woActionKey === 'MoveInstallWorkOrderFromInProgressToPending');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
console.log('Test: DELAY → MoveInstallWorkOrderToRemediation');
|
|
131
|
+
{
|
|
132
|
+
const decision = (0, kci_effect_bridge_1.decideKciDispatch)(makeProvideDto('DELAY'));
|
|
133
|
+
assert('kind=WO_FLOW_ACTION', decision.kind === 'WO_FLOW_ACTION');
|
|
134
|
+
if (decision.kind === 'WO_FLOW_ACTION') {
|
|
135
|
+
assert('woActionKey=MoveInstallWorkOrderToRemediation', decision.woActionKey === 'MoveInstallWorkOrderToRemediation');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
console.log('Test: RESUMED → MoveInstallWorkOrderFromPendingToInProgress');
|
|
139
|
+
{
|
|
140
|
+
const decision = (0, kci_effect_bridge_1.decideKciDispatch)(makeProvideDto('RESUMED'));
|
|
141
|
+
assert('kind=WO_FLOW_ACTION', decision.kind === 'WO_FLOW_ACTION');
|
|
142
|
+
if (decision.kind === 'WO_FLOW_ACTION') {
|
|
143
|
+
assert('woActionKey=MoveInstallWorkOrderFromPendingToInProgress', decision.woActionKey === 'MoveInstallWorkOrderFromPendingToInProgress');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
console.log('Test: REAPPOINTED → CreateInstallRescheduledChangeReasonSupplier');
|
|
147
|
+
{
|
|
148
|
+
const decision = (0, kci_effect_bridge_1.decideKciDispatch)(makeProvideDto('REAPPOINTED'));
|
|
149
|
+
assert('kind=WO_FLOW_ACTION', decision.kind === 'WO_FLOW_ACTION');
|
|
150
|
+
if (decision.kind === 'WO_FLOW_ACTION') {
|
|
151
|
+
assert('woActionKey=CreateInstallRescheduledChangeReasonSupplier', decision.woActionKey === 'CreateInstallRescheduledChangeReasonSupplier');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
console.log('Test: WARNING → LOG_KCI_ONLY');
|
|
155
|
+
{
|
|
156
|
+
const decision = (0, kci_effect_bridge_1.decideKciDispatch)(makeProvideDto('WARNING'));
|
|
157
|
+
assert('kind=LOG_KCI_ONLY', decision.kind === 'LOG_KCI_ONLY');
|
|
158
|
+
}
|
|
159
|
+
// ── 4. Bridge invariants ───────────────────────────────────────────
|
|
160
|
+
console.log('Test: every row has logAgainstOrder=true (Frank rule #4)');
|
|
161
|
+
{
|
|
162
|
+
const violations = kci_effect_bridge_1.KCI_EFFECT_ROWS.filter((r) => !r.logAgainstOrder);
|
|
163
|
+
assert('no logAgainstOrder violations', violations.length === 0, violations.length > 0 ? `violations: ${violations.map((r) => r.id).join(', ')}` : undefined);
|
|
164
|
+
}
|
|
165
|
+
console.log('Test: every WO_FLOW_ACTION row has woActionKey set');
|
|
166
|
+
{
|
|
167
|
+
const violations = kci_effect_bridge_1.KCI_EFFECT_ROWS.filter((r) => r.yfEffect.kind === 'WO_FLOW_ACTION' && !r.yfEffect.woActionKey);
|
|
168
|
+
assert('no missing woActionKey', violations.length === 0, violations.length > 0 ? `rows: ${violations.map((r) => r.id).join(', ')}` : undefined);
|
|
169
|
+
}
|
|
170
|
+
console.log('Test: every PROPERTY_WRITE row has propertyWrite set');
|
|
171
|
+
{
|
|
172
|
+
const violations = kci_effect_bridge_1.KCI_EFFECT_ROWS.filter((r) => r.yfEffect.kind === 'PROPERTY_WRITE' && !r.yfEffect.propertyWrite);
|
|
173
|
+
assert('no missing propertyWrite', violations.length === 0);
|
|
174
|
+
}
|
|
175
|
+
console.log('Test: row ids are unique');
|
|
176
|
+
{
|
|
177
|
+
const ids = new Set();
|
|
178
|
+
let dupes = 0;
|
|
179
|
+
for (const row of kci_effect_bridge_1.KCI_EFFECT_ROWS) {
|
|
180
|
+
if (ids.has(row.id))
|
|
181
|
+
dupes += 1;
|
|
182
|
+
ids.add(row.id);
|
|
183
|
+
}
|
|
184
|
+
assert('no duplicate ids', dupes === 0, dupes > 0 ? `${dupes} dupes` : undefined);
|
|
185
|
+
}
|
|
186
|
+
// ── 5. NO_MATCH for out-of-scope kinds (CEASE/MODIFY/UNSOLICITED) ──
|
|
187
|
+
console.log('Test: CEASE × COMPLETED returns NO_MATCH (out of scope)');
|
|
188
|
+
{
|
|
189
|
+
const dto = {
|
|
190
|
+
id: 'k',
|
|
191
|
+
sequenceNumber: 1,
|
|
192
|
+
issuedOn: '2026-05-09T00:00:00Z',
|
|
193
|
+
reasonCode: 'COMPLETED',
|
|
194
|
+
ceaseServiceOrder: { id: 'o', tenant: 'YF', status: 'COMPLETED' },
|
|
195
|
+
};
|
|
196
|
+
const decision = (0, kci_effect_bridge_1.decideKciDispatch)(dto);
|
|
197
|
+
assert('kind=NO_MATCH', decision.kind === 'NO_MATCH');
|
|
198
|
+
if (decision.kind === 'NO_MATCH') {
|
|
199
|
+
const k = decision.serviceOrderKind;
|
|
200
|
+
assert('serviceOrderKind=CEASE', k === 'CEASE');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
console.log('\n────────────────────────────────────────');
|
|
204
|
+
console.log(`Tests: ${passed} passed, ${failed} failed`);
|
|
205
|
+
if (failed > 0) {
|
|
206
|
+
console.log('');
|
|
207
|
+
failures.forEach((f) => console.log(f));
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
run();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d19n/youfibre-odin-sdk",
|
|
3
|
-
"version": "2.0.103-beta.
|
|
3
|
+
"version": "2.0.103-beta.14",
|
|
4
4
|
"description": "",
|
|
5
5
|
"author": "@d19n",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@d19n/odin-types": "^3.0.41"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
|
-
"test:wholesale-provider": "npx --yes tsx src/wholesale-provider/test/netomnia-provider.spec.ts",
|
|
35
|
+
"test:wholesale-provider": "npx --yes tsx src/wholesale-provider/test/netomnia-provider.spec.ts && npx --yes tsx src/wholesale-provider/test/kci-effect-bridge.spec.ts",
|
|
36
36
|
"release:beta": "git pull --ff-only && rm -rf dist && tsc && npm version prerelease --preid=beta && git push && npm publish --tag beta",
|
|
37
37
|
"release:prod": "git pull --ff-only && rm -rf dist && tsc && npm version patch && git push && npm publish --tag latest"
|
|
38
38
|
},
|