Some checks failed
CI / verify (push) Has been cancelled
Two bugs surfaced by the same failed finalize run for store 7311.
1. addPhoneNumbersToLocation was POSTing to /locations/{id}/numbers,
which Webex treats as "provision a NEW number" — that triggered
duplicate PSTN orders against numbers AE had already manually
ordered through Control Hub, and the provider rejected them with
ORDER_CREATION_FAILED. AE's workflow is manual ordering, so this
step now VERIFIES the number's current state in the org via
GET /telephony/config/numbers?phoneNumber=..., classifies the
outcome (already here / pending / wrong location / not in org),
and either succeeds silently or throws with a specific
Control-Hub fix-it instruction. The bot never triggers a PSTN
order now.
2. findDectNetworkInLocation was hitting
GET /telephony/config/locations/{id}/dectNetworks, which Webex
does not implement (returns HTTP 404 "No static resource ..."
unconditionally). That silently broke both the finalize
idempotency pre-check and the /provisionDect fallback, so
/provisionDect kept showing the "create network" card for stores
that already had one, then 409'd on the create attempt. Switched
to the real endpoint GET /telephony/config/dectNetworks with a
locationId filter. Also added translateCreateDectError to
rewrite the deeply-nested 27453 "access code in use" 409 blob
into an actionable "already exists, re-run /provisionDect"
message.
Extracted both error/state translators as pure exported functions
(classifyNumberAssignment, translateCreateDectError) with unit-test
coverage locking down the exact Webex payload shapes.
README: updated /finalizeStore to reflect the manual-order
expectation and specific fix-it instructions on mismatch.
Co-authored-by: Cursor <cursoragent@cursor.com>
106 lines
4.8 KiB
JavaScript
106 lines
4.8 KiB
JavaScript
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
|
|
import { classifyNumberAssignment, translateAddNumberError } from '../src/webex/locations.js';
|
|
|
|
const LOC = { id: 'LOC-STORE-7311', name: 'Store 7311' };
|
|
|
|
// The exact JSON Webex returns when a freshly-purchased number is
|
|
// still being provisioned by the carrier. Kept verbatim from a real
|
|
// error so this test breaks loudly if Webex ever changes the payload
|
|
// shape (and we can update our detection to match).
|
|
const PENDING_ORDERS_BODY =
|
|
'[{"errors":[{"errorType":"VERIFICATION","errorCode":"ERR.V.TRM.TMN60027","errorMessage":"Number has pending orders.","errorTitle":"NUMBER_HAS_PENDING_ORDERS"}]}]';
|
|
|
|
function fakeHttpError(status, body) {
|
|
const err = new Error(`HTTP ${status} for POST /whatever\n${body}`);
|
|
err.status = status;
|
|
err.body = body;
|
|
return err;
|
|
}
|
|
|
|
describe('translateAddNumberError', () => {
|
|
it('rewrites NUMBER_HAS_PENDING_ORDERS into an actionable operator message', () => {
|
|
const raw = fakeHttpError(400, PENDING_ORDERS_BODY);
|
|
const translated = translateAddNumberError(raw, '+14045551212');
|
|
assert.notEqual(translated, raw, 'should be a new Error instance');
|
|
assert.match(translated.message, /pending order/i);
|
|
assert.match(translated.message, /re-run \/finalizeStore/);
|
|
assert.match(translated.message, /\+14045551212/);
|
|
assert.equal(translated.status, 400);
|
|
assert.equal(translated.cause, raw, 'raw error should be preserved as .cause');
|
|
});
|
|
|
|
it('returns unrelated errors unchanged (same instance)', () => {
|
|
const raw = fakeHttpError(400, '{"message":"Nope","errorCode":9999}');
|
|
const result = translateAddNumberError(raw, '+14045551212');
|
|
assert.equal(result, raw);
|
|
assert.doesNotMatch(result.message, /pending order/i);
|
|
});
|
|
|
|
it('tolerates errors with no body property', () => {
|
|
const raw = new Error('ENOTFOUND webexapis.com');
|
|
assert.equal(translateAddNumberError(raw, '+14045551212'), raw);
|
|
});
|
|
});
|
|
|
|
// classifyNumberAssignment decides what to do about the number's
|
|
// current state in the org. The whole point of extracting it is so
|
|
// this branching is unit-testable without any HTTP; the four kinds
|
|
// here map 1:1 to the four operator-visible outcomes.
|
|
describe('classifyNumberAssignment', () => {
|
|
// Regression: the endpoint that used to run in this step
|
|
// (POST /telephony/config/locations/{id}/numbers) triggers a new
|
|
// PSTN order — which is exactly wrong for AE's manual-order
|
|
// workflow. The "already-here" path is the common case now and
|
|
// must be a clean no-op.
|
|
it("returns 'already-here' when the number is ACTIVE in this location", () => {
|
|
const existing = {
|
|
phoneNumber: '+14045551212',
|
|
state: 'ACTIVE',
|
|
location: { id: LOC.id, name: LOC.name },
|
|
};
|
|
const out = classifyNumberAssignment(existing, '+14045551212', LOC);
|
|
assert.equal(out.kind, 'already-here');
|
|
assert.equal(out.existing, existing);
|
|
});
|
|
|
|
it("returns 'pending' when the number is in the org but not ACTIVE yet", () => {
|
|
const existing = {
|
|
phoneNumber: '+14045551212',
|
|
state: 'PENDING',
|
|
location: { id: LOC.id, name: LOC.name },
|
|
};
|
|
const out = classifyNumberAssignment(existing, '+14045551212', LOC);
|
|
assert.equal(out.kind, 'pending');
|
|
assert.match(out.message, /still processing/i);
|
|
assert.match(out.message, /re-run \/finalizeStore/);
|
|
});
|
|
|
|
it("returns 'wrong-location' when the number is ACTIVE in a different location", () => {
|
|
const existing = {
|
|
phoneNumber: '+14045551212',
|
|
state: 'ACTIVE',
|
|
location: { id: 'LOC-OTHER', name: 'Store 0499' },
|
|
};
|
|
const out = classifyNumberAssignment(existing, '+14045551212', LOC);
|
|
assert.equal(out.kind, 'wrong-location');
|
|
assert.match(out.message, /Store 0499/); // current location shown
|
|
assert.match(out.message, /Store 7311/); // target location shown
|
|
assert.match(out.message, /Move it in Control Hub/);
|
|
});
|
|
|
|
it('treats a missing location on the record as wrong-location (unassigned)', () => {
|
|
const existing = { phoneNumber: '+14045551212', state: 'ACTIVE' };
|
|
const out = classifyNumberAssignment(existing, '+14045551212', LOC);
|
|
assert.equal(out.kind, 'wrong-location');
|
|
assert.match(out.message, /\(unassigned\)/);
|
|
});
|
|
|
|
it("returns 'not-in-org' when the number isn't in the inventory at all", () => {
|
|
const out = classifyNumberAssignment(null, '+14045551212', LOC);
|
|
assert.equal(out.kind, 'not-in-org');
|
|
assert.match(out.message, /Order it in Control Hub/);
|
|
assert.match(out.message, /Store 7311/);
|
|
});
|
|
});
|