@go-labs-sg/bb 2.41.0 → 2.43.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/README.md +33 -0
- package/command-manifest.json +132 -2
- package/command-reference.md +138 -2
- package/dist/index.js +637 -228
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6789,6 +6789,186 @@ var ExtendedErrorStatus = {
|
|
|
6789
6789
|
...ErrorStatus
|
|
6790
6790
|
};
|
|
6791
6791
|
|
|
6792
|
+
// src/runtime/error.ts
|
|
6793
|
+
var invocationErrorCodes = new Set([
|
|
6794
|
+
"USAGE",
|
|
6795
|
+
"VALIDATION",
|
|
6796
|
+
"CONFIRMATION_REQUIRED"
|
|
6797
|
+
]);
|
|
6798
|
+
var trpcCodeMap = {
|
|
6799
|
+
BAD_REQUEST: "VALIDATION",
|
|
6800
|
+
CONFLICT: "CONFLICT",
|
|
6801
|
+
FORBIDDEN: "AUTHORIZATION",
|
|
6802
|
+
INTERNAL_SERVER_ERROR: "API",
|
|
6803
|
+
METHOD_NOT_SUPPORTED: "API",
|
|
6804
|
+
NOT_FOUND: "NOT_FOUND",
|
|
6805
|
+
PARSE_ERROR: "VALIDATION",
|
|
6806
|
+
PAYLOAD_TOO_LARGE: "VALIDATION",
|
|
6807
|
+
PRECONDITION_FAILED: "CONFLICT",
|
|
6808
|
+
TIMEOUT: "NETWORK",
|
|
6809
|
+
TOO_MANY_REQUESTS: "API",
|
|
6810
|
+
UNAUTHORIZED: "AUTHENTICATION",
|
|
6811
|
+
UNPROCESSABLE_CONTENT: "VALIDATION"
|
|
6812
|
+
};
|
|
6813
|
+
var cliErrorCodes = new Set([
|
|
6814
|
+
"USAGE",
|
|
6815
|
+
"VALIDATION",
|
|
6816
|
+
"CONFIRMATION_REQUIRED",
|
|
6817
|
+
"AUTHENTICATION",
|
|
6818
|
+
"AUTHORIZATION",
|
|
6819
|
+
"NOT_FOUND",
|
|
6820
|
+
"CONFLICT",
|
|
6821
|
+
"API",
|
|
6822
|
+
"NETWORK",
|
|
6823
|
+
"ABORTED",
|
|
6824
|
+
"INTERRUPTED",
|
|
6825
|
+
"INTERNAL"
|
|
6826
|
+
]);
|
|
6827
|
+
|
|
6828
|
+
class CliRuntimeError extends Error {
|
|
6829
|
+
code;
|
|
6830
|
+
details;
|
|
6831
|
+
requestId;
|
|
6832
|
+
constructor(code, message, options = {}) {
|
|
6833
|
+
super(message);
|
|
6834
|
+
this.name = "CliRuntimeError";
|
|
6835
|
+
this.code = code;
|
|
6836
|
+
this.details = options.details;
|
|
6837
|
+
this.requestId = options.requestId;
|
|
6838
|
+
}
|
|
6839
|
+
}
|
|
6840
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6841
|
+
var asNonEmptyString = (value) => typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
6842
|
+
var formatValidationMessage = (message, data) => {
|
|
6843
|
+
const zodError = isRecord(data?.zodError) ? data.zodError : undefined;
|
|
6844
|
+
const fieldErrors = isRecord(zodError?.fieldErrors) ? zodError.fieldErrors : undefined;
|
|
6845
|
+
const fields = fieldErrors ? Object.entries(fieldErrors).flatMap(([field, errors]) => Array.isArray(errors) ? errors.filter((entry) => typeof entry === "string").map((entry) => `${field}: ${entry}`) : []) : [];
|
|
6846
|
+
const formErrors = Array.isArray(zodError?.formErrors) ? zodError.formErrors.filter((entry) => typeof entry === "string") : [];
|
|
6847
|
+
const validationIssues = [...fields, ...formErrors];
|
|
6848
|
+
if (validationIssues.length > 0)
|
|
6849
|
+
return validationIssues.join("; ");
|
|
6850
|
+
let parsed = message;
|
|
6851
|
+
for (let depth = 0;depth < 3 && typeof parsed === "string"; depth++) {
|
|
6852
|
+
try {
|
|
6853
|
+
parsed = JSON.parse(parsed);
|
|
6854
|
+
} catch {
|
|
6855
|
+
return message;
|
|
6856
|
+
}
|
|
6857
|
+
}
|
|
6858
|
+
if (!Array.isArray(parsed) || parsed.length === 0 || !parsed.every(isRecord))
|
|
6859
|
+
return message;
|
|
6860
|
+
const issues = parsed.map((issue) => {
|
|
6861
|
+
if (!Array.isArray(issue.path) || typeof issue.message !== "string" || typeof issue.code !== "string")
|
|
6862
|
+
return;
|
|
6863
|
+
const path = issue.path.filter((part) => typeof part === "string" || typeof part === "number").join(".") || "payload";
|
|
6864
|
+
if (issue.code === "invalid_type" && typeof issue.expected === "string" && issue.message.toLowerCase().includes("required")) {
|
|
6865
|
+
return `${path} must be a ${issue.expected}${path === "cost" ? ', e.g. "19"' : ""}`;
|
|
6866
|
+
}
|
|
6867
|
+
if (issue.code === "invalid_format" && issue.format === "regex" && path === "name" && String(issue.pattern ?? issue.message).includes("A-Za-z0-9,.?@&!#'~* _;+-")) {
|
|
6868
|
+
return `${path} contains characters that are not allowed. Allowed: letters, numbers, spaces and , . ? @ & ! # ' ~ * _ ; + -`;
|
|
6869
|
+
}
|
|
6870
|
+
return `${path}: ${issue.message}`;
|
|
6871
|
+
});
|
|
6872
|
+
return issues.every((issue) => issue !== undefined) ? issues.join("; ") : message;
|
|
6873
|
+
};
|
|
6874
|
+
var asDetails = (value) => {
|
|
6875
|
+
if (!isRecord(value))
|
|
6876
|
+
return;
|
|
6877
|
+
const details = {};
|
|
6878
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
6879
|
+
if (isJsonValue(nestedValue))
|
|
6880
|
+
details[key] = nestedValue;
|
|
6881
|
+
}
|
|
6882
|
+
return Object.keys(details).length > 0 ? details : undefined;
|
|
6883
|
+
};
|
|
6884
|
+
var isJsonValue = (value) => {
|
|
6885
|
+
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number") {
|
|
6886
|
+
return true;
|
|
6887
|
+
}
|
|
6888
|
+
if (Array.isArray(value))
|
|
6889
|
+
return value.every(isJsonValue);
|
|
6890
|
+
return isRecord(value) && Object.values(value).every(isJsonValue);
|
|
6891
|
+
};
|
|
6892
|
+
var mapErrorCode = (value) => {
|
|
6893
|
+
if (typeof value !== "string")
|
|
6894
|
+
return;
|
|
6895
|
+
if (cliErrorCodes.has(value))
|
|
6896
|
+
return value;
|
|
6897
|
+
if (value in trpcCodeMap)
|
|
6898
|
+
return trpcCodeMap[value];
|
|
6899
|
+
return;
|
|
6900
|
+
};
|
|
6901
|
+
var networkMessagePattern = /\b(fetch failed|network error|unable to connect|econnrefused|econnreset|enotfound|etimedout|socket hang up)\b/i;
|
|
6902
|
+
var isNetworkError = (error, depth = 0) => {
|
|
6903
|
+
if (depth > 3 || !isRecord(error))
|
|
6904
|
+
return false;
|
|
6905
|
+
const message = asNonEmptyString(error.message);
|
|
6906
|
+
if (message !== undefined && networkMessagePattern.test(message))
|
|
6907
|
+
return true;
|
|
6908
|
+
const code = asNonEmptyString(error.code);
|
|
6909
|
+
if (code !== undefined && networkMessagePattern.test(code))
|
|
6910
|
+
return true;
|
|
6911
|
+
return isNetworkError(error.cause, depth + 1);
|
|
6912
|
+
};
|
|
6913
|
+
var normalizeCliError = (error) => {
|
|
6914
|
+
if (error instanceof CliRuntimeError) {
|
|
6915
|
+
return {
|
|
6916
|
+
code: error.code,
|
|
6917
|
+
message: error.message,
|
|
6918
|
+
...error.details === undefined ? {} : { details: error.details },
|
|
6919
|
+
...error.requestId === undefined ? {} : { requestId: error.requestId }
|
|
6920
|
+
};
|
|
6921
|
+
}
|
|
6922
|
+
if (isNetworkError(error)) {
|
|
6923
|
+
return {
|
|
6924
|
+
code: "NETWORK",
|
|
6925
|
+
message: error instanceof Error ? error.message : "The Budget Builder API could not be reached."
|
|
6926
|
+
};
|
|
6927
|
+
}
|
|
6928
|
+
if (isRecord(error)) {
|
|
6929
|
+
const data = isRecord(error.data) ? error.data : undefined;
|
|
6930
|
+
const transportCode = mapErrorCode(data?.code ?? error.code);
|
|
6931
|
+
if (error instanceof Error && data === undefined && transportCode === undefined) {
|
|
6932
|
+
return { code: "INTERNAL", message: error.message };
|
|
6933
|
+
}
|
|
6934
|
+
const code = transportCode ?? "API";
|
|
6935
|
+
const rawMessage = asNonEmptyString(error.message) ?? "The Budget Builder API request failed.";
|
|
6936
|
+
const message = code === "VALIDATION" ? formatValidationMessage(rawMessage, data) : rawMessage;
|
|
6937
|
+
const requestId = asNonEmptyString(data?.requestId) ?? asNonEmptyString(data?.requestID) ?? asNonEmptyString(error.requestId);
|
|
6938
|
+
const details = asDetails(data?.details ?? error.details);
|
|
6939
|
+
return {
|
|
6940
|
+
code,
|
|
6941
|
+
message,
|
|
6942
|
+
...details === undefined ? {} : { details },
|
|
6943
|
+
...requestId === undefined ? {} : { requestId }
|
|
6944
|
+
};
|
|
6945
|
+
}
|
|
6946
|
+
if (error instanceof Error) {
|
|
6947
|
+
return { code: "INTERNAL", message: error.message };
|
|
6948
|
+
}
|
|
6949
|
+
return { code: "INTERNAL", message: "An unexpected CLI error occurred." };
|
|
6950
|
+
};
|
|
6951
|
+
var exitCodeForCliError = (error) => {
|
|
6952
|
+
if (error.code === "INTERRUPTED")
|
|
6953
|
+
return 130;
|
|
6954
|
+
return invocationErrorCodes.has(error.code) ? 2 : 1;
|
|
6955
|
+
};
|
|
6956
|
+
|
|
6957
|
+
// src/budget-create-outcome.ts
|
|
6958
|
+
var isP2028Error = (value, depth = 0) => {
|
|
6959
|
+
if (depth > 3 || typeof value !== "object" || value === null)
|
|
6960
|
+
return false;
|
|
6961
|
+
const error = value;
|
|
6962
|
+
return error.code === "P2028" || typeof error.message === "string" && /\bP2028\b/.test(error.message) || isP2028Error(error.cause, depth + 1) || isP2028Error(error.data, depth + 1) || isP2028Error(error.error, depth + 1);
|
|
6963
|
+
};
|
|
6964
|
+
var budgetCreateUncertainError = (error, input) => {
|
|
6965
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6966
|
+
const normalized = normalizeCliError(error);
|
|
6967
|
+
if (!isP2028Error(error) && normalized.code !== "NETWORK")
|
|
6968
|
+
return;
|
|
6969
|
+
return new CliRuntimeError(normalized.code === "NETWORK" ? "NETWORK" : "API", `Budget creation outcome is uncertain (the write may have committed). Run list-budgets --projectId ${input.projectId} and check for ${JSON.stringify(input.name)} before retrying; matching names alone may be ambiguous. Original error: ${message}`);
|
|
6970
|
+
};
|
|
6971
|
+
|
|
6792
6972
|
// src/commands/invoice-splits.ts
|
|
6793
6973
|
var invoiceSplitSetupNextAction = (budgetId) => ({
|
|
6794
6974
|
prompt: "Review the uninvoiced balance, choose all or part of the remaining percentage, then supply invoice and due dates, billing address, and customer message. Non-admin creation requests admin approval; admin creation auto-approves. Sending remains separate.",
|
|
@@ -7135,142 +7315,11 @@ var normalizePlainTextRichTextField = ({
|
|
|
7135
7315
|
};
|
|
7136
7316
|
};
|
|
7137
7317
|
|
|
7138
|
-
// src/runtime/error.ts
|
|
7139
|
-
var invocationErrorCodes = new Set([
|
|
7140
|
-
"USAGE",
|
|
7141
|
-
"VALIDATION",
|
|
7142
|
-
"CONFIRMATION_REQUIRED"
|
|
7143
|
-
]);
|
|
7144
|
-
var trpcCodeMap = {
|
|
7145
|
-
BAD_REQUEST: "VALIDATION",
|
|
7146
|
-
CONFLICT: "CONFLICT",
|
|
7147
|
-
FORBIDDEN: "AUTHORIZATION",
|
|
7148
|
-
INTERNAL_SERVER_ERROR: "API",
|
|
7149
|
-
METHOD_NOT_SUPPORTED: "API",
|
|
7150
|
-
NOT_FOUND: "NOT_FOUND",
|
|
7151
|
-
PARSE_ERROR: "VALIDATION",
|
|
7152
|
-
PAYLOAD_TOO_LARGE: "VALIDATION",
|
|
7153
|
-
PRECONDITION_FAILED: "CONFLICT",
|
|
7154
|
-
TIMEOUT: "NETWORK",
|
|
7155
|
-
TOO_MANY_REQUESTS: "API",
|
|
7156
|
-
UNAUTHORIZED: "AUTHENTICATION",
|
|
7157
|
-
UNPROCESSABLE_CONTENT: "VALIDATION"
|
|
7158
|
-
};
|
|
7159
|
-
var cliErrorCodes = new Set([
|
|
7160
|
-
"USAGE",
|
|
7161
|
-
"VALIDATION",
|
|
7162
|
-
"CONFIRMATION_REQUIRED",
|
|
7163
|
-
"AUTHENTICATION",
|
|
7164
|
-
"AUTHORIZATION",
|
|
7165
|
-
"NOT_FOUND",
|
|
7166
|
-
"CONFLICT",
|
|
7167
|
-
"API",
|
|
7168
|
-
"NETWORK",
|
|
7169
|
-
"ABORTED",
|
|
7170
|
-
"INTERRUPTED",
|
|
7171
|
-
"INTERNAL"
|
|
7172
|
-
]);
|
|
7173
|
-
|
|
7174
|
-
class CliRuntimeError extends Error {
|
|
7175
|
-
code;
|
|
7176
|
-
details;
|
|
7177
|
-
requestId;
|
|
7178
|
-
constructor(code, message, options = {}) {
|
|
7179
|
-
super(message);
|
|
7180
|
-
this.name = "CliRuntimeError";
|
|
7181
|
-
this.code = code;
|
|
7182
|
-
this.details = options.details;
|
|
7183
|
-
this.requestId = options.requestId;
|
|
7184
|
-
}
|
|
7185
|
-
}
|
|
7186
|
-
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7187
|
-
var asNonEmptyString = (value) => typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
7188
|
-
var asDetails = (value) => {
|
|
7189
|
-
if (!isRecord(value))
|
|
7190
|
-
return;
|
|
7191
|
-
const details = {};
|
|
7192
|
-
for (const [key, nestedValue] of Object.entries(value)) {
|
|
7193
|
-
if (isJsonValue(nestedValue))
|
|
7194
|
-
details[key] = nestedValue;
|
|
7195
|
-
}
|
|
7196
|
-
return Object.keys(details).length > 0 ? details : undefined;
|
|
7197
|
-
};
|
|
7198
|
-
var isJsonValue = (value) => {
|
|
7199
|
-
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number") {
|
|
7200
|
-
return true;
|
|
7201
|
-
}
|
|
7202
|
-
if (Array.isArray(value))
|
|
7203
|
-
return value.every(isJsonValue);
|
|
7204
|
-
return isRecord(value) && Object.values(value).every(isJsonValue);
|
|
7205
|
-
};
|
|
7206
|
-
var mapErrorCode = (value) => {
|
|
7207
|
-
if (typeof value !== "string")
|
|
7208
|
-
return;
|
|
7209
|
-
if (cliErrorCodes.has(value))
|
|
7210
|
-
return value;
|
|
7211
|
-
if (value in trpcCodeMap)
|
|
7212
|
-
return trpcCodeMap[value];
|
|
7213
|
-
return;
|
|
7214
|
-
};
|
|
7215
|
-
var networkMessagePattern = /\b(fetch failed|network error|unable to connect|econnrefused|econnreset|enotfound|etimedout|socket hang up)\b/i;
|
|
7216
|
-
var isNetworkError = (error, depth = 0) => {
|
|
7217
|
-
if (depth > 3 || !isRecord(error))
|
|
7218
|
-
return false;
|
|
7219
|
-
const message = asNonEmptyString(error.message);
|
|
7220
|
-
if (message !== undefined && networkMessagePattern.test(message))
|
|
7221
|
-
return true;
|
|
7222
|
-
const code = asNonEmptyString(error.code);
|
|
7223
|
-
if (code !== undefined && networkMessagePattern.test(code))
|
|
7224
|
-
return true;
|
|
7225
|
-
return isNetworkError(error.cause, depth + 1);
|
|
7226
|
-
};
|
|
7227
|
-
var normalizeCliError = (error) => {
|
|
7228
|
-
if (error instanceof CliRuntimeError) {
|
|
7229
|
-
return {
|
|
7230
|
-
code: error.code,
|
|
7231
|
-
message: error.message,
|
|
7232
|
-
...error.details === undefined ? {} : { details: error.details },
|
|
7233
|
-
...error.requestId === undefined ? {} : { requestId: error.requestId }
|
|
7234
|
-
};
|
|
7235
|
-
}
|
|
7236
|
-
if (isNetworkError(error)) {
|
|
7237
|
-
return {
|
|
7238
|
-
code: "NETWORK",
|
|
7239
|
-
message: error instanceof Error ? error.message : "The Budget Builder API could not be reached."
|
|
7240
|
-
};
|
|
7241
|
-
}
|
|
7242
|
-
if (isRecord(error)) {
|
|
7243
|
-
const data = isRecord(error.data) ? error.data : undefined;
|
|
7244
|
-
const transportCode = mapErrorCode(data?.code ?? error.code);
|
|
7245
|
-
if (error instanceof Error && data === undefined && transportCode === undefined) {
|
|
7246
|
-
return { code: "INTERNAL", message: error.message };
|
|
7247
|
-
}
|
|
7248
|
-
const code = transportCode ?? "API";
|
|
7249
|
-
const message = asNonEmptyString(error.message) ?? "The Budget Builder API request failed.";
|
|
7250
|
-
const requestId = asNonEmptyString(data?.requestId) ?? asNonEmptyString(data?.requestID) ?? asNonEmptyString(error.requestId);
|
|
7251
|
-
const details = asDetails(data?.details ?? error.details);
|
|
7252
|
-
return {
|
|
7253
|
-
code,
|
|
7254
|
-
message,
|
|
7255
|
-
...details === undefined ? {} : { details },
|
|
7256
|
-
...requestId === undefined ? {} : { requestId }
|
|
7257
|
-
};
|
|
7258
|
-
}
|
|
7259
|
-
if (error instanceof Error) {
|
|
7260
|
-
return { code: "INTERNAL", message: error.message };
|
|
7261
|
-
}
|
|
7262
|
-
return { code: "INTERNAL", message: "An unexpected CLI error occurred." };
|
|
7263
|
-
};
|
|
7264
|
-
var exitCodeForCliError = (error) => {
|
|
7265
|
-
if (error.code === "INTERRUPTED")
|
|
7266
|
-
return 130;
|
|
7267
|
-
return invocationErrorCodes.has(error.code) ? 2 : 1;
|
|
7268
|
-
};
|
|
7269
|
-
|
|
7270
7318
|
// src/parse-mutation-payload.ts
|
|
7319
|
+
var validationError = (message) => new CliRuntimeError("VALIDATION", message);
|
|
7271
7320
|
function requireObject(raw, label) {
|
|
7272
7321
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
7273
|
-
throw
|
|
7322
|
+
throw validationError(`${label} must be a JSON object.`);
|
|
7274
7323
|
}
|
|
7275
7324
|
return raw;
|
|
7276
7325
|
}
|
|
@@ -7283,7 +7332,7 @@ function toDate(value, field) {
|
|
|
7283
7332
|
if (!Number.isNaN(d.getTime()))
|
|
7284
7333
|
return d;
|
|
7285
7334
|
}
|
|
7286
|
-
throw
|
|
7335
|
+
throw validationError(`${field} must be an ISO date/datetime string.`);
|
|
7287
7336
|
}
|
|
7288
7337
|
function optionalString(v) {
|
|
7289
7338
|
if (v === undefined || v === null)
|
|
@@ -7297,14 +7346,14 @@ var parseQuotationReferences = (input, commandName) => {
|
|
|
7297
7346
|
let quotationAllocations;
|
|
7298
7347
|
if (rawAllocations !== undefined && rawAllocations !== null) {
|
|
7299
7348
|
if (!Array.isArray(rawAllocations) || rawAllocations.length === 0) {
|
|
7300
|
-
throw
|
|
7349
|
+
throw validationError(`${commandName} payload.quotationAllocations must be a non-empty array.`);
|
|
7301
7350
|
}
|
|
7302
7351
|
quotationAllocations = rawAllocations.map((raw, index) => {
|
|
7303
7352
|
const label = `${commandName} payload.quotationAllocations[${index}]`;
|
|
7304
7353
|
const allocation = requireObject(raw, label);
|
|
7305
7354
|
const amount = Number(allocation.amount);
|
|
7306
7355
|
if (!Number.isFinite(amount) || amount < 0 || !Number.isInteger(amount * 100)) {
|
|
7307
|
-
throw
|
|
7356
|
+
throw validationError(`${label}.amount must be a non-negative cent value.`);
|
|
7308
7357
|
}
|
|
7309
7358
|
return {
|
|
7310
7359
|
budgetItemId: requiredString(allocation.budgetItemId, `${label}.budgetItemId`),
|
|
@@ -7314,22 +7363,22 @@ var parseQuotationReferences = (input, commandName) => {
|
|
|
7314
7363
|
});
|
|
7315
7364
|
const keys = quotationAllocations.map((allocation) => `${allocation.budgetItemId}\x00${allocation.quotationId}`);
|
|
7316
7365
|
if (new Set(keys).size !== keys.length) {
|
|
7317
|
-
throw
|
|
7366
|
+
throw validationError(`${commandName} payload.quotationAllocations must not repeat a budget-item/quotation pair.`);
|
|
7318
7367
|
}
|
|
7319
7368
|
}
|
|
7320
7369
|
const rawQuotationIds = input.quotationIds;
|
|
7321
7370
|
let quotationIds;
|
|
7322
7371
|
if (rawQuotationIds !== undefined && rawQuotationIds !== null) {
|
|
7323
7372
|
if (!Array.isArray(rawQuotationIds)) {
|
|
7324
|
-
throw
|
|
7373
|
+
throw validationError(`${commandName} payload.quotationIds must be a string array.`);
|
|
7325
7374
|
}
|
|
7326
7375
|
quotationIds = rawQuotationIds.map((id, index) => requiredString(id, `${commandName} payload.quotationIds[${index}]`));
|
|
7327
7376
|
if (new Set(quotationIds).size !== quotationIds.length) {
|
|
7328
|
-
throw
|
|
7377
|
+
throw validationError(`${commandName} payload.quotationIds must not contain duplicates.`);
|
|
7329
7378
|
}
|
|
7330
7379
|
}
|
|
7331
7380
|
if (quotationId && quotationIds && (quotationIds.length !== 1 || quotationIds[0] !== quotationId)) {
|
|
7332
|
-
throw
|
|
7381
|
+
throw validationError(`${commandName} payload.quotationId conflicts with payload.quotationIds.`);
|
|
7333
7382
|
}
|
|
7334
7383
|
if (quotationAllocations && quotationIds) {
|
|
7335
7384
|
const allocationIds = [
|
|
@@ -7337,7 +7386,7 @@ var parseQuotationReferences = (input, commandName) => {
|
|
|
7337
7386
|
].sort();
|
|
7338
7387
|
const selectedIds = [...quotationIds].sort();
|
|
7339
7388
|
if (allocationIds.join("\x00") !== selectedIds.join("\x00")) {
|
|
7340
|
-
throw
|
|
7389
|
+
throw validationError(`${commandName} payload.quotationIds conflicts with payload.quotationAllocations.`);
|
|
7341
7390
|
}
|
|
7342
7391
|
}
|
|
7343
7392
|
return {
|
|
@@ -7350,14 +7399,14 @@ var parseBillLineAmounts = (input, commandName) => {
|
|
|
7350
7399
|
if (input.lineAmounts === undefined || input.lineAmounts === null)
|
|
7351
7400
|
return;
|
|
7352
7401
|
if (!Array.isArray(input.lineAmounts) || input.lineAmounts.length === 0) {
|
|
7353
|
-
throw
|
|
7402
|
+
throw validationError(`${commandName} payload.lineAmounts must be a non-empty array.`);
|
|
7354
7403
|
}
|
|
7355
7404
|
const lineAmounts = input.lineAmounts.map((raw, index) => {
|
|
7356
7405
|
const label = `${commandName} payload.lineAmounts[${index}]`;
|
|
7357
7406
|
const line = requireObject(raw, label);
|
|
7358
7407
|
const amount = Number(line.amount);
|
|
7359
7408
|
if (!Number.isFinite(amount) || amount < 0 || !Number.isInteger(amount * 100)) {
|
|
7360
|
-
throw
|
|
7409
|
+
throw validationError(`${label}.amount must be a non-negative cent value.`);
|
|
7361
7410
|
}
|
|
7362
7411
|
return {
|
|
7363
7412
|
budgetItemId: requiredString(line.budgetItemId, `${label}.budgetItemId`),
|
|
@@ -7365,7 +7414,7 @@ var parseBillLineAmounts = (input, commandName) => {
|
|
|
7365
7414
|
};
|
|
7366
7415
|
});
|
|
7367
7416
|
if (new Set(lineAmounts.map((line) => line.budgetItemId)).size !== lineAmounts.length) {
|
|
7368
|
-
throw
|
|
7417
|
+
throw validationError(`${commandName} payload.lineAmounts must contain each budget item once.`);
|
|
7369
7418
|
}
|
|
7370
7419
|
return lineAmounts;
|
|
7371
7420
|
};
|
|
@@ -7380,14 +7429,14 @@ var validateExplicitBillAllocations = ({
|
|
|
7380
7429
|
if (!lineAmounts && !quotationAllocations)
|
|
7381
7430
|
return;
|
|
7382
7431
|
if (!lineAmounts) {
|
|
7383
|
-
throw
|
|
7432
|
+
throw validationError(`${commandName} payload.lineAmounts is required with quotationAllocations.`);
|
|
7384
7433
|
}
|
|
7385
7434
|
const selected = new Set(budgetItemIds);
|
|
7386
7435
|
if (lineAmounts.length !== selected.size || lineAmounts.some((line) => !selected.has(line.budgetItemId))) {
|
|
7387
|
-
throw
|
|
7436
|
+
throw validationError(`${commandName} payload.lineAmounts must contain each selected budget item exactly once.`);
|
|
7388
7437
|
}
|
|
7389
7438
|
if (lineAmounts.reduce((sum, line) => sum + cents(line.amount), 0) !== cents(amount)) {
|
|
7390
|
-
throw
|
|
7439
|
+
throw validationError(`${commandName} payload.lineAmounts must sum to payload.amount.`);
|
|
7391
7440
|
}
|
|
7392
7441
|
if (!quotationAllocations)
|
|
7393
7442
|
return;
|
|
@@ -7395,27 +7444,27 @@ var validateExplicitBillAllocations = ({
|
|
|
7395
7444
|
const allocatedById = new Map;
|
|
7396
7445
|
for (const allocation of quotationAllocations) {
|
|
7397
7446
|
if (!selected.has(allocation.budgetItemId)) {
|
|
7398
|
-
throw
|
|
7447
|
+
throw validationError(`${commandName} payload.quotationAllocations references an unselected budget item.`);
|
|
7399
7448
|
}
|
|
7400
7449
|
allocatedById.set(allocation.budgetItemId, (allocatedById.get(allocation.budgetItemId) ?? 0) + cents(allocation.amount));
|
|
7401
7450
|
if (cents(allocation.amount) === 0 && lineAmountById.get(allocation.budgetItemId) !== 0) {
|
|
7402
|
-
throw
|
|
7451
|
+
throw validationError(`${commandName} zero quotation allocations are only valid for zero-value bill lines.`);
|
|
7403
7452
|
}
|
|
7404
7453
|
}
|
|
7405
7454
|
for (const [budgetItemId, allocated] of allocatedById) {
|
|
7406
7455
|
if (allocated > (lineAmountById.get(budgetItemId) ?? 0)) {
|
|
7407
|
-
throw
|
|
7456
|
+
throw validationError(`${commandName} quotation allocations exceed the line amount for ${budgetItemId}.`);
|
|
7408
7457
|
}
|
|
7409
7458
|
}
|
|
7410
7459
|
};
|
|
7411
7460
|
function requiredString(value, fieldLabel) {
|
|
7412
7461
|
if (value === undefined || value === null) {
|
|
7413
|
-
throw
|
|
7462
|
+
throw validationError(`${fieldLabel} is required.`);
|
|
7414
7463
|
}
|
|
7415
7464
|
const s = typeof value === "string" ? value : String(value);
|
|
7416
7465
|
const t = s.trim();
|
|
7417
7466
|
if (t === "") {
|
|
7418
|
-
throw
|
|
7467
|
+
throw validationError(`${fieldLabel} is required.`);
|
|
7419
7468
|
}
|
|
7420
7469
|
return t;
|
|
7421
7470
|
}
|
|
@@ -7423,13 +7472,13 @@ var EMAIL_ADDRESS_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
7423
7472
|
var requiredEmail = (value, fieldLabel) => {
|
|
7424
7473
|
const email = requiredString(value, fieldLabel);
|
|
7425
7474
|
if (!EMAIL_ADDRESS_PATTERN.test(email)) {
|
|
7426
|
-
throw
|
|
7475
|
+
throw validationError(`${fieldLabel} must be a valid email address.`);
|
|
7427
7476
|
}
|
|
7428
7477
|
return email;
|
|
7429
7478
|
};
|
|
7430
7479
|
var parseContactPersonEmailPayload = (input, commandName) => {
|
|
7431
7480
|
if (!Array.isArray(input.cc)) {
|
|
7432
|
-
throw
|
|
7481
|
+
throw validationError(`${commandName} payload.cc must be an email array.`);
|
|
7433
7482
|
}
|
|
7434
7483
|
return {
|
|
7435
7484
|
to: requiredEmail(input.to, `${commandName} payload.to`),
|
|
@@ -7459,7 +7508,7 @@ var parseSendCustomerInvoiceToContactPersonPayload = (raw) => {
|
|
|
7459
7508
|
function parseCreateBudgetPayload(raw) {
|
|
7460
7509
|
const o = requireObject(raw, "create-budget payload");
|
|
7461
7510
|
if (o.unavailableItemReviewAcknowledged !== undefined && typeof o.unavailableItemReviewAcknowledged !== "boolean") {
|
|
7462
|
-
throw
|
|
7511
|
+
throw validationError("create-budget payload.unavailableItemReviewAcknowledged must be a boolean when set.");
|
|
7463
7512
|
}
|
|
7464
7513
|
return {
|
|
7465
7514
|
name: requiredString(o.name, "create-budget payload.name"),
|
|
@@ -7493,7 +7542,7 @@ function parseBudgetDiscountPayload(raw) {
|
|
|
7493
7542
|
const o = requireObject(raw, "update-budget-discount payload");
|
|
7494
7543
|
const type = o.type;
|
|
7495
7544
|
if (type !== "percentage" && type !== "amount") {
|
|
7496
|
-
throw
|
|
7545
|
+
throw validationError('update-budget-discount payload.type must be "percentage" or "amount".');
|
|
7497
7546
|
}
|
|
7498
7547
|
return {
|
|
7499
7548
|
budgetId: requiredString(o.budgetId, "update-budget-discount payload.budgetId"),
|
|
@@ -7517,28 +7566,28 @@ function parseCreateBillPayload(raw, draftOnlyFlag, quotationExempt = false, req
|
|
|
7517
7566
|
const o = requireObject(raw, "create-bill payload");
|
|
7518
7567
|
const isClaimable = o.isClaimable;
|
|
7519
7568
|
if (typeof isClaimable !== "boolean") {
|
|
7520
|
-
throw
|
|
7569
|
+
throw validationError("create-bill payload.isClaimable must be boolean.");
|
|
7521
7570
|
}
|
|
7522
7571
|
const budgetItemIds = o.budgetItemIds;
|
|
7523
7572
|
if (!Array.isArray(budgetItemIds) || budgetItemIds.length === 0) {
|
|
7524
|
-
throw
|
|
7573
|
+
throw validationError("create-bill payload.budgetItemIds must be a non-empty string array.");
|
|
7525
7574
|
}
|
|
7526
7575
|
const attachmentsRaw = o.attachments;
|
|
7527
7576
|
const attachments = Array.isArray(attachmentsRaw) ? attachmentsRaw.map((x, i) => billAttachment(x, i)) : [];
|
|
7528
7577
|
const draftOnlyRaw = o.draftOnly;
|
|
7529
7578
|
if (draftOnlyRaw !== undefined && typeof draftOnlyRaw !== "boolean") {
|
|
7530
|
-
throw
|
|
7579
|
+
throw validationError("create-bill payload.draftOnly must be boolean.");
|
|
7531
7580
|
}
|
|
7532
7581
|
if (draftOnlyFlag !== undefined && draftOnlyRaw !== undefined && draftOnlyFlag !== draftOnlyRaw) {
|
|
7533
|
-
throw
|
|
7582
|
+
throw validationError("--draft-only conflicts with payload.draftOnly.");
|
|
7534
7583
|
}
|
|
7535
7584
|
const draftOnly = draftOnlyFlag ?? draftOnlyRaw;
|
|
7536
7585
|
const alreadyPaidRaw = o.alreadyPaid;
|
|
7537
7586
|
if (draftOnly && alreadyPaidRaw === true) {
|
|
7538
|
-
throw
|
|
7587
|
+
throw validationError("Draft-only creation cannot be combined with alreadyPaid: true.");
|
|
7539
7588
|
}
|
|
7540
7589
|
if (alreadyPaidRaw !== undefined && alreadyPaidRaw !== null && typeof alreadyPaidRaw !== "boolean") {
|
|
7541
|
-
throw
|
|
7590
|
+
throw validationError("create-bill payload.alreadyPaid must be boolean.");
|
|
7542
7591
|
}
|
|
7543
7592
|
const paymentProofAttachmentsRaw = o.paymentProofAttachments;
|
|
7544
7593
|
const paymentProofAttachments = Array.isArray(paymentProofAttachmentsRaw) ? paymentProofAttachmentsRaw.map((x, i) => billAttachment(x, i, "paymentProofAttachments")) : [];
|
|
@@ -7548,7 +7597,7 @@ function parseCreateBillPayload(raw, draftOnlyFlag, quotationExempt = false, req
|
|
|
7548
7597
|
const v = requireObject(vr, "verificationResults");
|
|
7549
7598
|
const verificationResultStatus = v.status;
|
|
7550
7599
|
if (verificationResultStatus !== BillVerificationStatus.PENDING && verificationResultStatus !== BillVerificationStatus.PASS && verificationResultStatus !== BillVerificationStatus.FAIL && verificationResultStatus !== BillVerificationStatus.MANUAL_REVIEW) {
|
|
7551
|
-
throw
|
|
7600
|
+
throw validationError("create-bill payload.verificationResults.status must be PENDING | PASS | FAIL | MANUAL_REVIEW.");
|
|
7552
7601
|
}
|
|
7553
7602
|
verificationResults = {
|
|
7554
7603
|
status: verificationResultStatus,
|
|
@@ -7563,11 +7612,11 @@ function parseCreateBillPayload(raw, draftOnlyFlag, quotationExempt = false, req
|
|
|
7563
7612
|
if (verificationStatus === BillVerificationStatus.PENDING || verificationStatus === BillVerificationStatus.PASS || verificationStatus === BillVerificationStatus.FAIL || verificationStatus === BillVerificationStatus.MANUAL_REVIEW) {
|
|
7564
7613
|
vs = verificationStatus;
|
|
7565
7614
|
} else if (verificationStatus !== undefined && verificationStatus !== null) {
|
|
7566
|
-
throw
|
|
7615
|
+
throw validationError("create-bill payload.verificationStatus must be PENDING | PASS | FAIL | MANUAL_REVIEW.");
|
|
7567
7616
|
}
|
|
7568
7617
|
const amount = Number(o.amount);
|
|
7569
7618
|
if (!Number.isFinite(amount) || amount < 0) {
|
|
7570
|
-
throw
|
|
7619
|
+
throw validationError("create-bill payload.amount must be a number >= 0.");
|
|
7571
7620
|
}
|
|
7572
7621
|
const quotationReferences = parseQuotationReferences(o, "create-bill");
|
|
7573
7622
|
const parsedBudgetItemIds = budgetItemIds.map((id, i) => requiredString(id, `create-bill payload.budgetItemIds[${i}]`));
|
|
@@ -7580,16 +7629,16 @@ function parseCreateBillPayload(raw, draftOnlyFlag, quotationExempt = false, req
|
|
|
7580
7629
|
quotationAllocations: quotationReferences.quotationAllocations
|
|
7581
7630
|
});
|
|
7582
7631
|
if (quotationExempt && isClaimable) {
|
|
7583
|
-
throw
|
|
7632
|
+
throw validationError("--quotation-exempt is only valid for supplier bills.");
|
|
7584
7633
|
}
|
|
7585
7634
|
if (quotationExempt && (quotationReferences.quotationId || (quotationReferences.quotationIds?.length ?? 0) > 0 || (quotationReferences.quotationAllocations?.length ?? 0) > 0)) {
|
|
7586
|
-
throw
|
|
7635
|
+
throw validationError("--quotation-exempt cannot be combined with payload quotation IDs.");
|
|
7587
7636
|
}
|
|
7588
7637
|
if (isClaimable && (quotationReferences.quotationId || (quotationReferences.quotationIds?.length ?? 0) > 0 || (quotationReferences.quotationAllocations?.length ?? 0) > 0)) {
|
|
7589
|
-
throw
|
|
7638
|
+
throw validationError("Employee claims cannot include quotation source inputs.");
|
|
7590
7639
|
}
|
|
7591
7640
|
if (requireSupplierQuotation && !quotationExempt && !isClaimable && !quotationReferences.quotationId && (quotationReferences.quotationIds?.length ?? 0) === 0 && (quotationReferences.quotationAllocations?.length ?? 0) === 0) {
|
|
7592
|
-
throw
|
|
7641
|
+
throw validationError("create-bill supplier bills require payload.quotationAllocations, payload.quotationIds, or legacy payload.quotationId.");
|
|
7593
7642
|
}
|
|
7594
7643
|
return {
|
|
7595
7644
|
budgetId: requiredString(o.budgetId, "create-bill payload.budgetId"),
|
|
@@ -7616,14 +7665,110 @@ function parseCreateBillPayload(raw, draftOnlyFlag, quotationExempt = false, req
|
|
|
7616
7665
|
}
|
|
7617
7666
|
function parseUpdateBillPayload(raw) {
|
|
7618
7667
|
const o = requireObject(raw, "update-bill payload");
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
throw new Error("draftOnly is only supported when creating a bill.");
|
|
7668
|
+
if (o.draftOnly !== undefined) {
|
|
7669
|
+
throw validationError("draftOnly is only supported when creating a bill.");
|
|
7622
7670
|
}
|
|
7623
|
-
|
|
7624
|
-
...input,
|
|
7671
|
+
const input = {
|
|
7625
7672
|
id: requiredString(o.id, "update-bill payload.id")
|
|
7626
7673
|
};
|
|
7674
|
+
const knownFields = new Set([
|
|
7675
|
+
"id",
|
|
7676
|
+
"budgetId",
|
|
7677
|
+
"projectId",
|
|
7678
|
+
"supplierId",
|
|
7679
|
+
"quotationId",
|
|
7680
|
+
"quotationIds",
|
|
7681
|
+
"quotationAllocations",
|
|
7682
|
+
"overBudgetOverrideReason",
|
|
7683
|
+
"amount",
|
|
7684
|
+
"budgetItemIds",
|
|
7685
|
+
"lineAmounts",
|
|
7686
|
+
"comment",
|
|
7687
|
+
"isClaimable",
|
|
7688
|
+
"attachments",
|
|
7689
|
+
"alreadyPaid",
|
|
7690
|
+
"paymentReference",
|
|
7691
|
+
"paymentProofAttachments",
|
|
7692
|
+
"extractedSupplierName",
|
|
7693
|
+
"extractedAmount",
|
|
7694
|
+
"invoiceNumber",
|
|
7695
|
+
"invoiceDate",
|
|
7696
|
+
"verificationStatus",
|
|
7697
|
+
"verificationResults"
|
|
7698
|
+
]);
|
|
7699
|
+
for (const key of Object.keys(o))
|
|
7700
|
+
if (!knownFields.has(key))
|
|
7701
|
+
throw validationError(`Unknown update-bill payload field: ${key}`);
|
|
7702
|
+
for (const key of [
|
|
7703
|
+
"budgetId",
|
|
7704
|
+
"projectId",
|
|
7705
|
+
"supplierId",
|
|
7706
|
+
"comment",
|
|
7707
|
+
"paymentReference",
|
|
7708
|
+
"extractedSupplierName",
|
|
7709
|
+
"invoiceNumber",
|
|
7710
|
+
"overBudgetOverrideReason"
|
|
7711
|
+
]) {
|
|
7712
|
+
if (o[key] === undefined)
|
|
7713
|
+
continue;
|
|
7714
|
+
if (typeof o[key] !== "string")
|
|
7715
|
+
throw validationError(`update-bill payload.${key} must be a string.`);
|
|
7716
|
+
input[key] = o[key];
|
|
7717
|
+
}
|
|
7718
|
+
for (const key of ["amount", "extractedAmount"]) {
|
|
7719
|
+
if (o[key] === undefined)
|
|
7720
|
+
continue;
|
|
7721
|
+
const amount = Number(o[key]);
|
|
7722
|
+
if (!Number.isFinite(amount) || amount < 0)
|
|
7723
|
+
throw validationError(`update-bill payload.${key} must be a number >= 0.`);
|
|
7724
|
+
input[key] = amount;
|
|
7725
|
+
}
|
|
7726
|
+
for (const key of ["isClaimable", "alreadyPaid"]) {
|
|
7727
|
+
if (o[key] === undefined)
|
|
7728
|
+
continue;
|
|
7729
|
+
if (typeof o[key] !== "boolean")
|
|
7730
|
+
throw validationError(`update-bill payload.${key} must be boolean.`);
|
|
7731
|
+
input[key] = o[key];
|
|
7732
|
+
}
|
|
7733
|
+
for (const key of ["attachments", "paymentProofAttachments"]) {
|
|
7734
|
+
const attachments = o[key];
|
|
7735
|
+
if (attachments === undefined)
|
|
7736
|
+
continue;
|
|
7737
|
+
if (!Array.isArray(attachments))
|
|
7738
|
+
throw validationError(`update-bill payload.${key} must be an array.`);
|
|
7739
|
+
input[key] = attachments.map((value, index) => billAttachment(value, index, key));
|
|
7740
|
+
}
|
|
7741
|
+
if (o.budgetItemIds !== undefined) {
|
|
7742
|
+
if (!Array.isArray(o.budgetItemIds) || !o.budgetItemIds.length)
|
|
7743
|
+
throw validationError("update-bill payload.budgetItemIds must be a non-empty array.");
|
|
7744
|
+
input.budgetItemIds = o.budgetItemIds.map((id, index) => requiredString(id, `update-bill payload.budgetItemIds[${index}]`));
|
|
7745
|
+
}
|
|
7746
|
+
Object.assign(input, parseQuotationReferences(o, "update-bill"));
|
|
7747
|
+
if (o.lineAmounts !== undefined)
|
|
7748
|
+
input.lineAmounts = parseBillLineAmounts(o, "update-bill");
|
|
7749
|
+
if (o.invoiceDate !== undefined)
|
|
7750
|
+
input.invoiceDate = toDate(o.invoiceDate, "invoiceDate");
|
|
7751
|
+
if (o.verificationStatus !== undefined) {
|
|
7752
|
+
if (!Object.values(BillVerificationStatus).includes(o.verificationStatus))
|
|
7753
|
+
throw validationError("Invalid bill verification status.");
|
|
7754
|
+
input.verificationStatus = o.verificationStatus;
|
|
7755
|
+
}
|
|
7756
|
+
if (o.verificationResults !== undefined) {
|
|
7757
|
+
const value = requireObject(o.verificationResults, "verificationResults");
|
|
7758
|
+
const status = value.status;
|
|
7759
|
+
if (status !== BillVerificationStatus.PENDING && status !== BillVerificationStatus.PASS && status !== BillVerificationStatus.FAIL && status !== BillVerificationStatus.MANUAL_REVIEW)
|
|
7760
|
+
throw validationError("Invalid verificationResults.status.");
|
|
7761
|
+
input.verificationResults = {
|
|
7762
|
+
status,
|
|
7763
|
+
extractedAmount: value.extractedAmount == null ? null : Number(value.extractedAmount),
|
|
7764
|
+
difference: value.difference == null ? null : Number(value.difference),
|
|
7765
|
+
confidence: value.confidence == null ? null : Number(value.confidence),
|
|
7766
|
+
failureReason: value.failureReason == null ? null : String(value.failureReason)
|
|
7767
|
+
};
|
|
7768
|
+
}
|
|
7769
|
+
if (Object.keys(input).length === 1)
|
|
7770
|
+
throw validationError("Supply at least one field to update.");
|
|
7771
|
+
return input;
|
|
7627
7772
|
}
|
|
7628
7773
|
function parseValidateBillSelectionPayload(raw, quotationFlags = {}) {
|
|
7629
7774
|
const input = requireObject(raw, "validate-bill-selection payload");
|
|
@@ -7632,21 +7777,21 @@ function parseValidateBillSelectionPayload(raw, quotationFlags = {}) {
|
|
|
7632
7777
|
const payloadIds = payloadReferences.quotationIds ?? (payloadReferences.quotationId ? [payloadReferences.quotationId] : undefined);
|
|
7633
7778
|
const flagIds = flagReferences.quotationIds ?? (flagReferences.quotationId ? [flagReferences.quotationId] : undefined);
|
|
7634
7779
|
if (payloadIds && flagIds && payloadIds.join("\x00") !== flagIds.join("\x00")) {
|
|
7635
|
-
throw
|
|
7780
|
+
throw validationError("validate-bill-selection quotation flags conflict with the payload quotation IDs.");
|
|
7636
7781
|
}
|
|
7637
7782
|
const isClaimable = input.isClaimable;
|
|
7638
7783
|
if (isClaimable !== undefined && typeof isClaimable !== "boolean") {
|
|
7639
|
-
throw
|
|
7784
|
+
throw validationError("validate-bill-selection payload.isClaimable must be boolean.");
|
|
7640
7785
|
}
|
|
7641
7786
|
if (isClaimable === true && (payloadReferences.quotationId || (payloadReferences.quotationIds?.length ?? 0) > 0 || (payloadReferences.quotationAllocations?.length ?? 0) > 0 || !!flagReferences.quotationId || (flagReferences.quotationIds?.length ?? 0) > 0)) {
|
|
7642
|
-
throw
|
|
7787
|
+
throw validationError("Employee claims cannot include quotation source inputs.");
|
|
7643
7788
|
}
|
|
7644
7789
|
const lineAmounts = parseBillLineAmounts(input, "validate-bill-selection");
|
|
7645
7790
|
const budgetItemIds = Array.isArray(input.budgetItemIds) ? input.budgetItemIds.map((id, index) => requiredString(id, `validate-bill-selection payload.budgetItemIds[${index}]`)) : [];
|
|
7646
7791
|
const amount = input.amount === undefined || input.amount === null ? undefined : Number(input.amount);
|
|
7647
7792
|
if (lineAmounts || payloadReferences.quotationAllocations) {
|
|
7648
7793
|
if (amount === undefined || !Number.isFinite(amount) || budgetItemIds.length === 0) {
|
|
7649
|
-
throw
|
|
7794
|
+
throw validationError("validate-bill-selection explicit allocations require payload.amount and payload.budgetItemIds.");
|
|
7650
7795
|
}
|
|
7651
7796
|
validateExplicitBillAllocations({
|
|
7652
7797
|
amount,
|
|
@@ -7669,10 +7814,10 @@ function parseLinkBillQuotationsPayload(billId, raw) {
|
|
|
7669
7814
|
const input = requireObject(raw, "link-bill-quotations payload");
|
|
7670
7815
|
const references = parseQuotationReferences(input, "link-bill-quotations");
|
|
7671
7816
|
if (references.quotationId) {
|
|
7672
|
-
throw
|
|
7817
|
+
throw validationError("link-bill-quotations payload uses quotationIds or quotationAllocations; legacy quotationId is not supported.");
|
|
7673
7818
|
}
|
|
7674
7819
|
if ((references.quotationIds?.length ?? 0) === 0 && (references.quotationAllocations?.length ?? 0) === 0) {
|
|
7675
|
-
throw
|
|
7820
|
+
throw validationError("link-bill-quotations payload requires quotationIds or quotationAllocations.");
|
|
7676
7821
|
}
|
|
7677
7822
|
return {
|
|
7678
7823
|
billId: requiredString(billId, "billId"),
|
|
@@ -7725,14 +7870,14 @@ function parseUpdateQuotationPayload(raw) {
|
|
|
7725
7870
|
var parseCreateCustomerInvoicePayload = (raw) => {
|
|
7726
7871
|
const input = requireObject(raw, "create-customer-invoice payload");
|
|
7727
7872
|
if (!Array.isArray(input.splits) || input.splits.length !== 1) {
|
|
7728
|
-
throw
|
|
7873
|
+
throw validationError("create-customer-invoice payload.splits must contain exactly one invoice.");
|
|
7729
7874
|
}
|
|
7730
7875
|
if (input.isCustomPercentage !== undefined && typeof input.isCustomPercentage !== "boolean") {
|
|
7731
|
-
throw
|
|
7876
|
+
throw validationError("create-customer-invoice payload.isCustomPercentage must be a boolean.");
|
|
7732
7877
|
}
|
|
7733
7878
|
const details = input.details === undefined ? undefined : requireObject(input.details, "invoice details");
|
|
7734
7879
|
if (details && typeof details.billingAddress !== "string")
|
|
7735
|
-
throw
|
|
7880
|
+
throw validationError("invoice details.billingAddress must be a string.");
|
|
7736
7881
|
return {
|
|
7737
7882
|
...input.requestId === undefined ? {} : { requestId: requiredString(input.requestId, "requestId") },
|
|
7738
7883
|
...input.invoiceDate === undefined ? {} : { invoiceDate: toDate(input.invoiceDate, "invoiceDate") },
|
|
@@ -7748,7 +7893,7 @@ var parseCreateCustomerInvoicePayload = (raw) => {
|
|
|
7748
7893
|
const split = requireObject(rawSplit, `create-customer-invoice payload.splits[${index}]`);
|
|
7749
7894
|
const percentage = Number(split.percentage);
|
|
7750
7895
|
if (!Number.isFinite(percentage) || percentage <= 0 || percentage > 100 || Math.round(percentage * 100) !== percentage * 100) {
|
|
7751
|
-
throw
|
|
7896
|
+
throw validationError(`create-customer-invoice payload.splits[${index}].percentage must be between 0.01 and 100 with at most two decimal places.`);
|
|
7752
7897
|
}
|
|
7753
7898
|
return {
|
|
7754
7899
|
dueDate: toDate(split.dueDate, `create-customer-invoice payload.splits[${index}].dueDate`),
|
|
@@ -7762,7 +7907,7 @@ function parseUpdateProjectPayload(raw) {
|
|
|
7762
7907
|
const o = requireObject(raw, "update-project payload");
|
|
7763
7908
|
const dr = o.dateRange;
|
|
7764
7909
|
if (dr === null || typeof dr !== "object" || Array.isArray(dr)) {
|
|
7765
|
-
throw
|
|
7910
|
+
throw validationError("update-project payload.dateRange must be an object.");
|
|
7766
7911
|
}
|
|
7767
7912
|
const range = dr;
|
|
7768
7913
|
const notification = o.requestQboAccountantNotification;
|
|
@@ -7770,14 +7915,14 @@ function parseUpdateProjectPayload(raw) {
|
|
|
7770
7915
|
if (typeof notification === "boolean") {
|
|
7771
7916
|
requestQboAccountantNotification = notification;
|
|
7772
7917
|
} else if (notification !== undefined && notification !== null) {
|
|
7773
|
-
throw
|
|
7918
|
+
throw validationError("update-project payload.requestQboAccountantNotification must be a boolean when set.");
|
|
7774
7919
|
}
|
|
7775
7920
|
const estimateInvalidation = o.confirmEstimateInvalidation;
|
|
7776
7921
|
let confirmEstimateInvalidation;
|
|
7777
7922
|
if (typeof estimateInvalidation === "boolean") {
|
|
7778
7923
|
confirmEstimateInvalidation = estimateInvalidation;
|
|
7779
7924
|
} else if (estimateInvalidation !== undefined && estimateInvalidation !== null) {
|
|
7780
|
-
throw
|
|
7925
|
+
throw validationError("update-project payload.confirmEstimateInvalidation must be a boolean when set.");
|
|
7781
7926
|
}
|
|
7782
7927
|
const slackChannelId = optionalString(o.slackChannelId);
|
|
7783
7928
|
const slackChannelUrl = optionalString(o.slackChannelUrl);
|
|
@@ -7820,21 +7965,30 @@ function parseSupplierUpdatePayload(raw) {
|
|
|
7820
7965
|
requireObject(raw, "update-supplier payload");
|
|
7821
7966
|
return raw;
|
|
7822
7967
|
}
|
|
7823
|
-
|
|
7824
|
-
|
|
7968
|
+
var normalizeItemPayload = (input, command) => {
|
|
7969
|
+
if (typeof input.name === "string") {
|
|
7970
|
+
const invalidCharacters = [
|
|
7971
|
+
...new Set([...input.name].filter((character) => !/^[A-Za-z0-9,.?@&!#'~* _;+-]$/.test(character)))
|
|
7972
|
+
];
|
|
7973
|
+
if (invalidCharacters.length > 0) {
|
|
7974
|
+
throw validationError(`${command} payload.name contains characters that are not allowed: ${invalidCharacters.join(" ")}. Allowed: letters, numbers, spaces and , . ? @ & ! # ' ~ * _ ; + -`);
|
|
7975
|
+
}
|
|
7976
|
+
}
|
|
7977
|
+
const normalized = {
|
|
7978
|
+
...input,
|
|
7979
|
+
...typeof input.cost === "number" && Number.isFinite(input.cost) ? { cost: String(input.cost) } : {}
|
|
7980
|
+
};
|
|
7825
7981
|
return normalizePlainTextRichTextField({
|
|
7826
|
-
input,
|
|
7982
|
+
input: normalized,
|
|
7827
7983
|
textField: "description",
|
|
7828
7984
|
richTextField: "descriptionRichText"
|
|
7829
7985
|
});
|
|
7986
|
+
};
|
|
7987
|
+
function parseItemCreatePayload(raw) {
|
|
7988
|
+
return normalizeItemPayload(requireObject(raw, "create-item payload"), "create-item");
|
|
7830
7989
|
}
|
|
7831
7990
|
function parseItemUpdatePayload(raw) {
|
|
7832
|
-
|
|
7833
|
-
return normalizePlainTextRichTextField({
|
|
7834
|
-
input,
|
|
7835
|
-
textField: "description",
|
|
7836
|
-
richTextField: "descriptionRichText"
|
|
7837
|
-
});
|
|
7991
|
+
return normalizeItemPayload(requireObject(raw, "update-item payload"), "update-item");
|
|
7838
7992
|
}
|
|
7839
7993
|
function parseContactCreatePayload(raw) {
|
|
7840
7994
|
requireObject(raw, "create-contact-person payload");
|
|
@@ -8211,10 +8365,10 @@ var formatAsanaCandidateMetadata = (candidate) => {
|
|
|
8211
8365
|
};
|
|
8212
8366
|
var promptForAsanaLeadCandidate = async (candidates) => {
|
|
8213
8367
|
if (candidates.length === 0) {
|
|
8214
|
-
throw new Error("
|
|
8368
|
+
throw new Error("create-project requires an existing Asana deal task in the Deals project; none matched. For a new direct client, create the deal task using the website form first, then pass --asanaTaskId or --asanaSearch. The Slack channel comes from that task.");
|
|
8215
8369
|
}
|
|
8216
8370
|
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
8217
|
-
throw new Error("create-project requires --asanaTaskId in non-interactive runs.
|
|
8371
|
+
throw new Error("create-project requires an existing Asana deal task: pass --asanaTaskId in non-interactive runs. Create the deal task using the website form first for a new direct client. The Slack channel comes from that task; interactive runs can choose from matching tasks.");
|
|
8218
8372
|
}
|
|
8219
8373
|
console.error("Select the Asana lead task to link to this project:");
|
|
8220
8374
|
candidates.forEach((candidate, index) => {
|
|
@@ -8778,6 +8932,42 @@ async function updateBillStatus(opts) {
|
|
|
8778
8932
|
} : undefined);
|
|
8779
8933
|
}
|
|
8780
8934
|
}
|
|
8935
|
+
var listExternalBillPaymentAccounts = async () => {
|
|
8936
|
+
out(await api2.bill.getExternalPaymentAccounts.query());
|
|
8937
|
+
};
|
|
8938
|
+
var recordExternalBillPayment = async (opts) => {
|
|
8939
|
+
if (!opts.paymentReference.trim() || !opts.paymentAccount.trim() || !/^\d{4}-\d{2}-\d{2}$/.test(opts.paymentDate) || new Date(`${opts.paymentDate}T00:00:00Z`).toISOString().slice(0, 10) !== opts.paymentDate)
|
|
8940
|
+
throw new Error("Supply --paymentReference, --paymentDate YYYY-MM-DD, and --paymentAccount <QBO account ID>.");
|
|
8941
|
+
await assertSensitiveWorkflowConfirmed({
|
|
8942
|
+
action: "Record external payment (no email)",
|
|
8943
|
+
entity: `bill ${opts.id}`,
|
|
8944
|
+
details: `already paid on ${opts.paymentDate}; reference ${opts.paymentReference}; QBO account ${opts.paymentAccount}. No money is transferred.`
|
|
8945
|
+
});
|
|
8946
|
+
const bill = await api2.bill.getById.query({ id: opts.id });
|
|
8947
|
+
const proof = !bill.externalPayment && opts.paymentProofPath ? await stageBillPaymentProof(bill.projectId, opts.paymentProofPath) : undefined;
|
|
8948
|
+
try {
|
|
8949
|
+
out(await api2.bill.recordExternalPayment.mutate({
|
|
8950
|
+
id: opts.id,
|
|
8951
|
+
paymentReference: opts.paymentReference,
|
|
8952
|
+
paymentDate: opts.paymentDate,
|
|
8953
|
+
paymentAccount: opts.paymentAccount,
|
|
8954
|
+
paymentProofAttachments: proof ? [proof] : undefined
|
|
8955
|
+
}));
|
|
8956
|
+
} catch (error) {
|
|
8957
|
+
try {
|
|
8958
|
+
const latest = await api2.bill.getById.query({ id: opts.id });
|
|
8959
|
+
if (latest.externalPayment?.paymentAccount === opts.paymentAccount && latest.paymentReference === opts.paymentReference.trim() && latest.paymentDate?.toISOString().slice(0, 10) === opts.paymentDate) {
|
|
8960
|
+
out({
|
|
8961
|
+
...latest,
|
|
8962
|
+
supplierNotified: false,
|
|
8963
|
+
quickbooksReconciliationPending: !latest.externalPayment.quickbooksBillPaymentIds.length
|
|
8964
|
+
});
|
|
8965
|
+
return;
|
|
8966
|
+
}
|
|
8967
|
+
} catch {}
|
|
8968
|
+
throw new Error("External payment result could not be confirmed. Inspect the bill, then retry the same reference/date/account; do not pay again. Any uploaded proof is retained.", { cause: error });
|
|
8969
|
+
}
|
|
8970
|
+
};
|
|
8781
8971
|
async function patchBillPayment(opts) {
|
|
8782
8972
|
const input = { id: opts.id };
|
|
8783
8973
|
if (opts.paymentTrackingUrl !== undefined)
|
|
@@ -9141,7 +9331,15 @@ async function getBudgetDetails(budgetId) {
|
|
|
9141
9331
|
}
|
|
9142
9332
|
async function createBudgetFromPayload(raw) {
|
|
9143
9333
|
const input = parseCreateBudgetPayload(raw);
|
|
9144
|
-
|
|
9334
|
+
let result;
|
|
9335
|
+
try {
|
|
9336
|
+
result = await api2.budget.createBudget.mutate(input);
|
|
9337
|
+
} catch (error) {
|
|
9338
|
+
const uncertain = budgetCreateUncertainError(error, input);
|
|
9339
|
+
if (uncertain)
|
|
9340
|
+
throw uncertain;
|
|
9341
|
+
throw error;
|
|
9342
|
+
}
|
|
9145
9343
|
out(result);
|
|
9146
9344
|
}
|
|
9147
9345
|
async function updateBudgetFromPayload(raw) {
|
|
@@ -9543,31 +9741,44 @@ async function deleteBillById(id) {
|
|
|
9543
9741
|
}
|
|
9544
9742
|
async function createSupplierFromPayload(raw) {
|
|
9545
9743
|
const input = parseSupplierCreatePayload(raw);
|
|
9546
|
-
|
|
9744
|
+
await assertSensitiveWorkflowConfirmed({
|
|
9745
|
+
action: "Create supplier and request approval if required",
|
|
9746
|
+
entity: `supplier ${input.name}`,
|
|
9747
|
+
details: "creates the supplier first; pending-approval suppliers also request approval and send emails"
|
|
9748
|
+
});
|
|
9749
|
+
let created;
|
|
9750
|
+
try {
|
|
9751
|
+
created = await api2.supplier.createSupplier.mutate(input);
|
|
9752
|
+
} catch (error) {
|
|
9753
|
+
const normalized = normalizeCliError(error);
|
|
9754
|
+
if (normalized.code !== "NETWORK" && normalized.code !== "API") {
|
|
9755
|
+
throw error;
|
|
9756
|
+
}
|
|
9757
|
+
throw new CliRuntimeError(normalized.code, `Supplier creation outcome is uncertain for ${JSON.stringify(input.name)}. Run list-suppliers and inspect matching supplier details before retrying; names may not be unique, so do not retry create-supplier until you can verify that no supplier was created. Original error: ${normalized.message}`);
|
|
9758
|
+
}
|
|
9547
9759
|
const supplier = created.result;
|
|
9548
9760
|
if (supplier.status !== ApprovalStatus.PENDING_APPROVAL) {
|
|
9549
9761
|
out(created);
|
|
9550
9762
|
return;
|
|
9551
9763
|
}
|
|
9552
|
-
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
|
|
9556
|
-
|
|
9557
|
-
|
|
9558
|
-
|
|
9559
|
-
}
|
|
9764
|
+
let approval;
|
|
9765
|
+
try {
|
|
9766
|
+
approval = await api2.supplier.createSupplierApproval.mutate({
|
|
9767
|
+
supplierId: supplier.id
|
|
9768
|
+
});
|
|
9769
|
+
} catch (error) {
|
|
9770
|
+
throw new CliRuntimeError("API", `Supplier ${supplier.id} was created, but the approval request did not complete or its outcome is uncertain. Run get-supplier-details ${supplier.id} and inspect its approvals before retrying; do not run create-supplier again. Original error: ${normalizeCliError(error).message}`);
|
|
9771
|
+
}
|
|
9560
9772
|
let emailResult;
|
|
9561
9773
|
const approvalIds = (approval.results ?? []).map((item) => ({ id: item.id }));
|
|
9562
9774
|
if (approvalIds.length === 0) {
|
|
9563
|
-
|
|
9564
|
-
}
|
|
9565
|
-
|
|
9566
|
-
|
|
9567
|
-
|
|
9568
|
-
|
|
9569
|
-
|
|
9570
|
-
}
|
|
9775
|
+
throw new CliRuntimeError("API", `Supplier ${supplier.id} was created, but the approval request returned no approval IDs. Run get-supplier-details ${supplier.id} and inspect its approvals before retrying the approval request; do not run create-supplier again.`);
|
|
9776
|
+
}
|
|
9777
|
+
try {
|
|
9778
|
+
const delivery = approval.emailDelivery;
|
|
9779
|
+
emailResult = createApprovalEmailDeliveryReport(delivery);
|
|
9780
|
+
} catch (error) {
|
|
9781
|
+
emailResult = createApprovalEmailDeliveryFailure(error instanceof Error ? error.message : String(error));
|
|
9571
9782
|
}
|
|
9572
9783
|
out({
|
|
9573
9784
|
...created,
|
|
@@ -10312,24 +10523,167 @@ function getFlag(flags, key, parse) {
|
|
|
10312
10523
|
// src/parse-json-flag.ts
|
|
10313
10524
|
function parseJsonFlag(raw, label) {
|
|
10314
10525
|
if (raw === undefined || raw === "") {
|
|
10315
|
-
throw new
|
|
10526
|
+
throw new CliRuntimeError("VALIDATION", `${label} is required (JSON string).`);
|
|
10316
10527
|
}
|
|
10317
10528
|
try {
|
|
10318
10529
|
return JSON.parse(raw);
|
|
10319
10530
|
} catch {
|
|
10320
|
-
throw new
|
|
10531
|
+
throw new CliRuntimeError("VALIDATION", `${label} must be valid JSON.`);
|
|
10321
10532
|
}
|
|
10322
10533
|
}
|
|
10534
|
+
function parseJsonObjectFlag(raw, label) {
|
|
10535
|
+
const value = parseJsonFlag(raw, label);
|
|
10536
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
10537
|
+
throw new CliRuntimeError("VALIDATION", `${label} must be a JSON object.`);
|
|
10538
|
+
}
|
|
10539
|
+
return value;
|
|
10540
|
+
}
|
|
10323
10541
|
function parseOptionalNumber(raw, label) {
|
|
10324
10542
|
if (raw === undefined || raw === "")
|
|
10325
10543
|
return;
|
|
10326
10544
|
const n = typeof raw === "number" ? raw : Number.parseFloat(String(raw).trim());
|
|
10327
10545
|
if (!Number.isFinite(n)) {
|
|
10328
|
-
throw new
|
|
10546
|
+
throw new CliRuntimeError("VALIDATION", `${label} must be a number.`);
|
|
10329
10547
|
}
|
|
10330
10548
|
return n;
|
|
10331
10549
|
}
|
|
10332
10550
|
|
|
10551
|
+
// src/registry/payload-help.ts
|
|
10552
|
+
var payloadHelp = {
|
|
10553
|
+
"create-budget": {
|
|
10554
|
+
required: "name: string; budget: decimal string; paymentTerm: string; categoryId: string; projectId: string",
|
|
10555
|
+
optional: "sourceBudgetId: string; unavailableItemReviewAcknowledged: boolean (only after review); pipedriveDealId: string",
|
|
10556
|
+
example: '{"name":"Event budget","budget":"1500","paymentTerm":"Net 30","categoryId":"<category-id>","projectId":"<project-id>"}'
|
|
10557
|
+
},
|
|
10558
|
+
"update-budget": {
|
|
10559
|
+
required: "id: string; name: string; budget: decimal string; paymentTerm: string; categoryId: string; projectId: string",
|
|
10560
|
+
optional: "sourceBudgetId: string; pipedriveDealId: string",
|
|
10561
|
+
example: '{"id":"<budget-id>","name":"Event budget","budget":"1500","paymentTerm":"Net 30","categoryId":"<category-id>","projectId":"<project-id>"}'
|
|
10562
|
+
},
|
|
10563
|
+
"create-item": {
|
|
10564
|
+
required: "name: string (QuickBooks-safe characters); categoryId: string; supplierId: string; cost: decimal string or number",
|
|
10565
|
+
optional: "description: string; gstInclusive: boolean; gstOutOfScope: boolean; goLabsProduct: boolean; attachments: array",
|
|
10566
|
+
example: '{"name":"Event Crew per hour","categoryId":"<category-id>","supplierId":"<supplier-id>","cost":19}'
|
|
10567
|
+
},
|
|
10568
|
+
"update-item": {
|
|
10569
|
+
required: "id: string; name: string; categoryId: string; supplierId: string; cost: decimal string or number",
|
|
10570
|
+
optional: "description: string; gstInclusive: boolean; gstOutOfScope: boolean; goLabsProduct: boolean; attachments: array",
|
|
10571
|
+
example: '{"id":"<item-id>","name":"Event Crew per hour","categoryId":"<category-id>","supplierId":"<supplier-id>","cost":"19"}'
|
|
10572
|
+
},
|
|
10573
|
+
"create-contact-person": {
|
|
10574
|
+
required: "companyId: string; title: MR|MRS|MS|DR; firstName: string; lastName: string|null; email: string; mobile: string",
|
|
10575
|
+
optional: "address: {streetAddress1?, city?, state?, postalCode?, country?}",
|
|
10576
|
+
example: '{"companyId":"<company-id>","title":"MS","firstName":"Jane","lastName":"Tan","email":"jane@example.com","mobile":"+65 9123 4567"}'
|
|
10577
|
+
},
|
|
10578
|
+
"update-contact-person": {
|
|
10579
|
+
required: "id: string; companyId: string; title: MR|MRS|MS|DR; firstName: string; lastName: string|null; email: string; mobile: string",
|
|
10580
|
+
optional: "address: object",
|
|
10581
|
+
example: '{"id":"<contact-id>","companyId":"<company-id>","title":"MS","firstName":"Jane","lastName":"Tan","email":"jane@example.com","mobile":"+65 9123 4567"}'
|
|
10582
|
+
},
|
|
10583
|
+
"update-company": {
|
|
10584
|
+
required: "id: string; name: string; website: URL string",
|
|
10585
|
+
optional: "confirmDistinct: boolean",
|
|
10586
|
+
example: '{"id":"<company-id>","name":"Example Co","website":"https://example.com"}'
|
|
10587
|
+
},
|
|
10588
|
+
"update-project": {
|
|
10589
|
+
required: "id: string; name: string; companyId: string; contactPersonId: string; asanaTaskId: string; insideSalesId: string; businessDevelopmentId: string; venue: string; dateRange: {from: ISO date}",
|
|
10590
|
+
optional: "dateRange.to: ISO date; projectManagerId: string; pax: string; description: string; slackChannelId/slackChannelUrl/slackChannelName: strings (supply all three); requestQboAccountantNotification: boolean; confirmEstimateInvalidation: boolean",
|
|
10591
|
+
example: '{"id":"<project-id>","name":"Launch","companyId":"<company-id>","contactPersonId":"<contact-id>","asanaTaskId":"<asana-task-id>","insideSalesId":"<user-id>","businessDevelopmentId":"<user-id>","venue":"Hall","dateRange":{"from":"2026-10-01T09:00:00Z"}}'
|
|
10592
|
+
},
|
|
10593
|
+
"create-supplier": {
|
|
10594
|
+
required: "name: string; primaryContactPerson: string; email: string; phone: string; paymentMethodId: string; paymentTerm: string; roleId: string",
|
|
10595
|
+
optional: "gstRegistered: boolean; tags: string[]; website: URL string; address: object; attachments: array; bank and PayNow fields",
|
|
10596
|
+
example: '{"name":"Example Supplier","primaryContactPerson":"Jane Tan","email":"jane@example.com","phone":"91234567","paymentMethodId":"<method-id>","paymentTerm":"Net 30","roleId":"<role-id>"}'
|
|
10597
|
+
},
|
|
10598
|
+
"update-supplier": {
|
|
10599
|
+
required: "id: string; name: string; primaryContactPerson: string; email: string; phone: string; paymentMethodId: string; paymentTerm: string; roleId: string",
|
|
10600
|
+
optional: "gstRegistered: boolean; tags: string[]; website: URL string; address: object; attachments: array; bank and PayNow fields",
|
|
10601
|
+
example: '{"id":"<supplier-id>","name":"Example Supplier","primaryContactPerson":"Jane Tan","email":"jane@example.com","phone":"91234567","paymentMethodId":"<method-id>","paymentTerm":"Net 30","roleId":"<role-id>"}'
|
|
10602
|
+
},
|
|
10603
|
+
"update-budget-commission": {
|
|
10604
|
+
required: "budgetId: string; name: string; percentage: decimal string; totalSellingAfterDiscount: number",
|
|
10605
|
+
optional: "none",
|
|
10606
|
+
example: '{"budgetId":"<budget-id>","name":"Sales commission","percentage":"10","totalSellingAfterDiscount":1500}'
|
|
10607
|
+
},
|
|
10608
|
+
"update-budget-discount": {
|
|
10609
|
+
required: "budgetId: string; name: string; type: percentage|amount; value: decimal string; totalSellingBeforeDiscount: number",
|
|
10610
|
+
optional: "none",
|
|
10611
|
+
example: '{"budgetId":"<budget-id>","name":"Discount","type":"amount","value":"100","totalSellingBeforeDiscount":1500}'
|
|
10612
|
+
},
|
|
10613
|
+
"send-estimate-to-contact-person": {
|
|
10614
|
+
required: "estimateId: string; budgetId: string; to: email; cc: email[]; replyTo: email; subject: string; content: HTML string; signature: HTML string",
|
|
10615
|
+
optional: "estimateDocNumber: string",
|
|
10616
|
+
example: '{"estimateId":"<estimate-id>","budgetId":"<budget-id>","to":"client@example.com","cc":[],"replyTo":"team@example.com","subject":"Estimate","content":"<p>Attached estimate</p>","signature":"<p>Team</p>"}'
|
|
10617
|
+
},
|
|
10618
|
+
"create-bill": {
|
|
10619
|
+
required: "budgetId: string; projectId: string; supplierId: string; amount: number (ex-GST); budgetItemIds: string[]; isClaimable: boolean; supplier bills: quotationIds: string[] or quotationAllocations: {budgetItemId, quotationId, amount}[] (unless --quotation-exempt)",
|
|
10620
|
+
optional: "lineAmounts: {budgetItemId, amount}[] (required with quotationAllocations); attachments: {id,name,key,size}[]; invoiceNumber: string; invoiceDate: ISO date; draftOnly: boolean; alreadyPaid: boolean; extractedAmount: number",
|
|
10621
|
+
example: '{"budgetId":"<budget-id>","projectId":"<project-id>","supplierId":"<supplier-id>","amount":1800,"budgetItemIds":["<budget-item-id>"],"isClaimable":false,"quotationIds":["<quotation-id>"],"attachments":[]}'
|
|
10622
|
+
},
|
|
10623
|
+
"update-bill": {
|
|
10624
|
+
required: "id: string; budgetId: string; projectId: string; supplierId: string; amount: number (ex-GST); budgetItemIds: string[]; isClaimable: boolean",
|
|
10625
|
+
optional: "quotationIds: string[]; quotationAllocations: {budgetItemId, quotationId, amount}[]; lineAmounts: {budgetItemId, amount}[]; attachments: {id,name,key,size}[]; invoiceNumber: string; invoiceDate: ISO date",
|
|
10626
|
+
example: '{"id":"<bill-id>","budgetId":"<budget-id>","projectId":"<project-id>","supplierId":"<supplier-id>","amount":1800,"budgetItemIds":["<budget-item-id>"],"isClaimable":false,"quotationIds":["<quotation-id>"],"attachments":[]}'
|
|
10627
|
+
},
|
|
10628
|
+
"validate-bill-selection": {
|
|
10629
|
+
required: "projectId: string; supplierId: string; budgetItemIds: string[]",
|
|
10630
|
+
optional: "quotationIds: string[] for supplier bills; isClaimable: boolean; amount: number; lineAmounts: {budgetItemId, amount}[]; quotationAllocations: {budgetItemId, quotationId, amount}[] (requires amount and lineAmounts)",
|
|
10631
|
+
example: '{"projectId":"<project-id>","supplierId":"<supplier-id>","budgetItemIds":["<budget-item-id>"],"quotationIds":["<quotation-id>"]}'
|
|
10632
|
+
},
|
|
10633
|
+
"link-bill-quotations": {
|
|
10634
|
+
required: "positional <billId>; quotationIds: string[] or quotationAllocations: {budgetItemId, quotationId, amount}[]",
|
|
10635
|
+
optional: "none (legacy quotationId is not supported)",
|
|
10636
|
+
example: '{"quotationIds":["<quotation-id>"]}'
|
|
10637
|
+
},
|
|
10638
|
+
"update-bill-payment-evidence": {
|
|
10639
|
+
required: "id: string; paymentReference: string; paymentProofAttachments: {id,name (PDF),key,size}[] (at least one)",
|
|
10640
|
+
optional: "none",
|
|
10641
|
+
example: '{"id":"<bill-id>","paymentReference":"TRANSFER-123","paymentProofAttachments":[{"id":"<attachment-id>","name":"receipt.pdf","key":"<staged-key>","size":1000}]}'
|
|
10642
|
+
},
|
|
10643
|
+
"create-quotation": {
|
|
10644
|
+
required: "projectId: string; budgetId: string; supplierId: string; budgetItemIds: string[]; amountWithoutGst: number; gstAmount: number; attachments: array (uploaded attachment)",
|
|
10645
|
+
optional: "lineAmounts: {budgetItemId, amount}[] (required for multiple budget items)",
|
|
10646
|
+
example: '{"projectId":"<project-id>","budgetId":"<budget-id>","supplierId":"<supplier-id>","budgetItemIds":["<budget-item-id>"],"amountWithoutGst":1200,"gstAmount":0,"attachments":[{"id":"<attachment-id>","name":"quote.pdf","key":"<staged-key>","size":1000}]}'
|
|
10647
|
+
},
|
|
10648
|
+
"update-quotation": {
|
|
10649
|
+
required: "id: string; projectId: string; budgetId: string; supplierId: string; budgetItemIds: string[]; amountWithoutGst: number; gstAmount: number; attachments: array",
|
|
10650
|
+
optional: "lineAmounts: {budgetItemId, amount}[] (required for multiple budget items)",
|
|
10651
|
+
example: '{"id":"<quotation-id>","projectId":"<project-id>","budgetId":"<budget-id>","supplierId":"<supplier-id>","budgetItemIds":["<budget-item-id>"],"amountWithoutGst":1200,"gstAmount":0,"attachments":[{"id":"<attachment-id>","name":"quote.pdf","key":"<staged-key>","size":1000}]}'
|
|
10652
|
+
},
|
|
10653
|
+
"create-customer-invoice": {
|
|
10654
|
+
required: "budgetId: string; splits: [{label: string, percentage: number, dueDate: ISO date}] (exactly one)",
|
|
10655
|
+
optional: "requestId: UUID string (recommended); invoiceDate: ISO date; details: {billingAddress: string, customerMemo: string}; isCustomPercentage: boolean",
|
|
10656
|
+
example: '{"budgetId":"<budget-id>","splits":[{"label":"Deposit","percentage":50,"dueDate":"2026-10-15"}],"details":{"billingAddress":"1 Example St","customerMemo":"Thanks"}}'
|
|
10657
|
+
},
|
|
10658
|
+
"send-customer-invoice-to-contact-person": {
|
|
10659
|
+
required: "invoiceId: string; to: email; cc: email[]; replyTo: email; subject: string; content: HTML string; signature: HTML string",
|
|
10660
|
+
optional: "none",
|
|
10661
|
+
example: '{"invoiceId":"<invoice-id>","to":"client@example.com","cc":[],"replyTo":"team@example.com","subject":"Invoice","content":"<p>Attached invoice</p>","signature":"<p>Team</p>"}'
|
|
10662
|
+
},
|
|
10663
|
+
"save-inbound-mapping": {
|
|
10664
|
+
required: "positional <sourceId>; payload: [{canonicalField: enum, typeformFieldId: string, required?: boolean, typeformFieldRef?: string|null}] (1-50 entries)",
|
|
10665
|
+
optional: "required defaults false; typeformFieldRef may be omitted",
|
|
10666
|
+
example: '[{"canonicalField":"CONTACT_FIRST_NAME","typeformFieldId":"<typeform-field-id>"}]'
|
|
10667
|
+
},
|
|
10668
|
+
"save-inbound-automation-config": {
|
|
10669
|
+
required: "positional <sourceId>; asanaCustomFields: object; asanaDefaultAssigneeId: string|null; asanaDefaultAssigneeName: string|null; asanaFollowerIds: string[]; asanaProjectId: string; asanaSectionId: string; asanaWonSectionId: string; asanaLostSectionId: string; googleCalendarId: string; googleCalendarTimeZone: string; goshBriefBaseUrl: URL; slackDefaultAssigneeId: string|null; slackInboundChannelId: string; slackInviteUserIds: string[]; slackPitchChannelPrefix: string",
|
|
10670
|
+
optional: "asanaDiscussionSectionId, asanaFirstContactSectionId, asanaFirstProposalSentSectionId, asanaProposalWipSectionId: string|null; asanaCustomFields.*: mapped IDs",
|
|
10671
|
+
example: '{"asanaCustomFields":{},"asanaDefaultAssigneeId":null,"asanaDefaultAssigneeName":null,"asanaFollowerIds":[],"asanaProjectId":"<asana-project-id>","asanaSectionId":"<asana-section-id>","asanaWonSectionId":"<asana-won-id>","asanaLostSectionId":"<asana-lost-id>","googleCalendarId":"team@example.com","googleCalendarTimeZone":"Asia/Singapore","goshBriefBaseUrl":"https://example.com/brief","slackDefaultAssigneeId":null,"slackInboundChannelId":"<slack-channel-id>","slackInviteUserIds":[],"slackPitchChannelPrefix":"pitch"}'
|
|
10672
|
+
}
|
|
10673
|
+
};
|
|
10674
|
+
var formatPayloadHelp = (command) => {
|
|
10675
|
+
const help = payloadHelp[command];
|
|
10676
|
+
if (!help)
|
|
10677
|
+
return [];
|
|
10678
|
+
return [
|
|
10679
|
+
"Payload fields (--payload JSON)",
|
|
10680
|
+
` Required: ${help.required}`,
|
|
10681
|
+
` Optional: ${help.optional}`,
|
|
10682
|
+
` Example: bb ${command}${command === "link-bill-quotations" ? " <billId>" : command.startsWith("save-inbound-") ? " <sourceId>" : ""} --payload '${help.example}'`,
|
|
10683
|
+
""
|
|
10684
|
+
];
|
|
10685
|
+
};
|
|
10686
|
+
|
|
10333
10687
|
// src/registry/index.ts
|
|
10334
10688
|
var globalCommandOptions = [
|
|
10335
10689
|
{ name: "--help", description: "Show help for this command." },
|
|
@@ -10380,6 +10734,7 @@ var mutationPrefixes = [
|
|
|
10380
10734
|
"patch-",
|
|
10381
10735
|
"reactivate-",
|
|
10382
10736
|
"reconcile-",
|
|
10737
|
+
"record-",
|
|
10383
10738
|
"reject-",
|
|
10384
10739
|
"remove-",
|
|
10385
10740
|
"replace-",
|
|
@@ -10420,6 +10775,7 @@ var emailTargets = new Set([
|
|
|
10420
10775
|
"update-supplier"
|
|
10421
10776
|
]);
|
|
10422
10777
|
var externalWriteTargets = new Set([
|
|
10778
|
+
"record-external-bill-payment",
|
|
10423
10779
|
"approve-bill",
|
|
10424
10780
|
"approve-customer-invoice",
|
|
10425
10781
|
"approve-supplier",
|
|
@@ -10465,6 +10821,7 @@ var externalWriteTargets = new Set([
|
|
|
10465
10821
|
"void-customer-invoice"
|
|
10466
10822
|
]);
|
|
10467
10823
|
var financialWriteTargets = new Set([
|
|
10824
|
+
"record-external-bill-payment",
|
|
10468
10825
|
"import-qbo-customer-invoice",
|
|
10469
10826
|
"link-bill-quotations",
|
|
10470
10827
|
"approve-bill",
|
|
@@ -10530,6 +10887,8 @@ var summaryFor = (legacyTarget) => {
|
|
|
10530
10887
|
return "Reuse a company in BB and QuickBooks, creating only its missing counterpart. Supply --companyId <bbId> and/or --quickbooksId <qboId>; both explicitly connect records with different names. --website <url> fills a missing QBO import website only. --confirmDistinct true confirms no similar record represents this entity.";
|
|
10531
10888
|
if (legacyTarget === "create-company")
|
|
10532
10889
|
return "Create or reuse a company in BB and QuickBooks. --name <name> --website <url>. Similar names require --confirmDistinct true; inactive companies are never reactivated.";
|
|
10890
|
+
if (legacyTarget === "create-project")
|
|
10891
|
+
return "Create a project linked to an existing Asana Deals lead task. Supply --asanaTaskId in non-interactive use; interactive use searches for and selects a lead task (optionally --asanaSearch). The Slack channel is resolved from the selected Asana task unless all three Slack channel flags are supplied. For a new direct client, create the lead task using the website form first.";
|
|
10533
10892
|
if (legacyTarget === "preview-qbo-customer-invoice") {
|
|
10534
10893
|
return "Preview an available QuickBooks invoice selected for a BB budget using read-only QBO access. Arguments: <budgetId> <qboInvoiceId>.";
|
|
10535
10894
|
}
|
|
@@ -10547,8 +10906,12 @@ var summaryFor = (legacyTarget) => {
|
|
|
10547
10906
|
if (legacyTarget === "create-bill") {
|
|
10548
10907
|
return "Create a bill or claim with an approved supplier. All amount fields exclude GST: amount is the portion billed; extractedAmount is the full invoice subtotal; lineAmounts and quotationAllocations also exclude GST. Example: 1800 + 162 GST = 1962 total requires amount=1800 and extractedAmount=1800 for a full bill. The CLI does not strip GST from input. --draft-only guarantees DRAFT without email or external writes; otherwise unpaid creation skips email permission but keeps external-write permission.";
|
|
10549
10908
|
}
|
|
10909
|
+
if (legacyTarget === "record-external-bill-payment")
|
|
10910
|
+
return "Record an already-made payment on an APPROVED bill or claim, with no email and no money transfer. Requires <billId> --paymentReference --paymentDate YYYY-MM-DD --paymentAccount <QBO account ID> [--paymentProof receipt.pdf]. Reuses settled QBO payment links or queues one dated payment from the selected account. Exact retries are safe; do not pay again while reconciliation is pending.";
|
|
10911
|
+
if (legacyTarget === "list-external-payment-accounts")
|
|
10912
|
+
return "List active QuickBooks bank and credit-card account IDs for recording external payments (admin only).";
|
|
10550
10913
|
if (legacyTarget === "update-bill") {
|
|
10551
|
-
return "Update a bill or claim
|
|
10914
|
+
return "Update a bill or claim with a partial payload including id; omitted fields are preserved. Comment-only updates are allowed on approved/paid bills without altering financial data or QuickBooks. All amount fields exclude GST: amount is the portion billed; extractedAmount is the full invoice subtotal; lineAmounts and quotationAllocations also exclude GST. Example: 1800 + 162 GST = 1962 total requires amount=1800 and extractedAmount=1800 for a full bill. The CLI does not strip GST from input. Re-read and verify both amount fields and allocations after correcting a gross/net mistake.";
|
|
10552
10915
|
}
|
|
10553
10916
|
if (legacyTarget === "validate-bill-selection") {
|
|
10554
10917
|
return "Validate the approved supplier and selected bill line items.";
|
|
@@ -10587,6 +10950,20 @@ var legacyCommand = (legacyTarget, path, aliases = [], commandOptions = []) => (
|
|
|
10587
10950
|
effects: effectsFor(legacyTarget),
|
|
10588
10951
|
legacyTarget
|
|
10589
10952
|
});
|
|
10953
|
+
var budgetPreflightOptions = [
|
|
10954
|
+
{
|
|
10955
|
+
name: "--companyPayload <json>",
|
|
10956
|
+
description: "Company update JSON object with id, name, and website fields."
|
|
10957
|
+
},
|
|
10958
|
+
{
|
|
10959
|
+
name: "--contactPayload <json>",
|
|
10960
|
+
description: "Contact update JSON object with companyId, title, firstName, lastName, email, and mobile fields."
|
|
10961
|
+
},
|
|
10962
|
+
{
|
|
10963
|
+
name: "--eventPayload <json>",
|
|
10964
|
+
description: "Event timing JSON object with date, startTime, and endTime fields."
|
|
10965
|
+
}
|
|
10966
|
+
];
|
|
10590
10967
|
var authCommand = (action, summary) => ({
|
|
10591
10968
|
path: ["auth", action],
|
|
10592
10969
|
legacyAliases: [],
|
|
@@ -10611,8 +10988,8 @@ var registry2 = [
|
|
|
10611
10988
|
legacyCommand("rename-budget-version", ["budget", "version", "rename"]),
|
|
10612
10989
|
legacyCommand("restore-budget-version", ["budget", "version", "restore"]),
|
|
10613
10990
|
legacyCommand("update-budget-status", ["budget", "status", "update"]),
|
|
10614
|
-
legacyCommand("create-budget-approval", ["budget", "approval", "create"]),
|
|
10615
|
-
legacyCommand("create-estimate", ["budget", "estimate", "create"]),
|
|
10991
|
+
legacyCommand("create-budget-approval", ["budget", "approval", "create"], [], budgetPreflightOptions),
|
|
10992
|
+
legacyCommand("create-estimate", ["budget", "estimate", "create"], [], budgetPreflightOptions),
|
|
10616
10993
|
legacyCommand("send-estimate-to-contact-person", ["budget", "estimate", "send"], ["send-estimate-email"]),
|
|
10617
10994
|
legacyCommand("create-budget", ["budget", "create"]),
|
|
10618
10995
|
legacyCommand("update-budget", ["budget", "update"]),
|
|
@@ -10694,6 +11071,16 @@ var registry2 = [
|
|
|
10694
11071
|
]),
|
|
10695
11072
|
legacyCommand("delete-bill", ["bill", "delete"]),
|
|
10696
11073
|
legacyCommand("update-bill-status", ["bill", "status", "update"]),
|
|
11074
|
+
legacyCommand("record-external-bill-payment", [
|
|
11075
|
+
"bill",
|
|
11076
|
+
"payment",
|
|
11077
|
+
"record-external"
|
|
11078
|
+
]),
|
|
11079
|
+
legacyCommand("list-external-payment-accounts", [
|
|
11080
|
+
"bill",
|
|
11081
|
+
"payment",
|
|
11082
|
+
"accounts"
|
|
11083
|
+
]),
|
|
10697
11084
|
legacyCommand("patch-bill-payment", ["bill", "payment", "patch"]),
|
|
10698
11085
|
legacyCommand("patch-bill-invoice-number", [
|
|
10699
11086
|
"bill",
|
|
@@ -11046,6 +11433,7 @@ var createHumanHelp = (command) => {
|
|
|
11046
11433
|
...command.options.map((option) => ` ${option.name}
|
|
11047
11434
|
${option.description}`),
|
|
11048
11435
|
"",
|
|
11436
|
+
...formatPayloadHelp(command.legacyTarget ?? ""),
|
|
11049
11437
|
"Use `bb help --legacy` for additional command arguments."
|
|
11050
11438
|
].join(`
|
|
11051
11439
|
`);
|
|
@@ -11932,9 +12320,9 @@ Waiting for approval\u2026
|
|
|
11932
12320
|
const contactPayloadRaw = getFlag(flags, "contactPayload");
|
|
11933
12321
|
const eventPayloadRaw = getFlag(flags, "eventPayload");
|
|
11934
12322
|
await createBudgetApproval(id, {
|
|
11935
|
-
company: companyPayloadRaw !== undefined ?
|
|
11936
|
-
contactPerson: contactPayloadRaw !== undefined ?
|
|
11937
|
-
eventDetails: eventPayloadRaw !== undefined ?
|
|
12323
|
+
company: companyPayloadRaw !== undefined ? parseJsonObjectFlag(String(companyPayloadRaw), "--companyPayload") : undefined,
|
|
12324
|
+
contactPerson: contactPayloadRaw !== undefined ? parseJsonObjectFlag(String(contactPayloadRaw), "--contactPayload") : undefined,
|
|
12325
|
+
eventDetails: eventPayloadRaw !== undefined ? parseJsonObjectFlag(String(eventPayloadRaw), "--eventPayload") : undefined
|
|
11938
12326
|
});
|
|
11939
12327
|
break;
|
|
11940
12328
|
}
|
|
@@ -11946,9 +12334,9 @@ Waiting for approval\u2026
|
|
|
11946
12334
|
const contactPayloadRaw = getFlag(flags, "contactPayload");
|
|
11947
12335
|
const eventPayloadRaw = getFlag(flags, "eventPayload");
|
|
11948
12336
|
await createEstimate(id, {
|
|
11949
|
-
company: companyPayloadRaw !== undefined ?
|
|
11950
|
-
contactPerson: contactPayloadRaw !== undefined ?
|
|
11951
|
-
eventDetails: eventPayloadRaw !== undefined ?
|
|
12337
|
+
company: companyPayloadRaw !== undefined ? parseJsonObjectFlag(String(companyPayloadRaw), "--companyPayload") : undefined,
|
|
12338
|
+
contactPerson: contactPayloadRaw !== undefined ? parseJsonObjectFlag(String(contactPayloadRaw), "--contactPayload") : undefined,
|
|
12339
|
+
eventDetails: eventPayloadRaw !== undefined ? parseJsonObjectFlag(String(eventPayloadRaw), "--eventPayload") : undefined
|
|
11952
12340
|
});
|
|
11953
12341
|
break;
|
|
11954
12342
|
}
|
|
@@ -12269,6 +12657,27 @@ Waiting for approval\u2026
|
|
|
12269
12657
|
});
|
|
12270
12658
|
break;
|
|
12271
12659
|
}
|
|
12660
|
+
case "list-external-payment-accounts": {
|
|
12661
|
+
await listExternalBillPaymentAccounts();
|
|
12662
|
+
break;
|
|
12663
|
+
}
|
|
12664
|
+
case "record-external-bill-payment": {
|
|
12665
|
+
const id = positional[0];
|
|
12666
|
+
const paymentReference = getFlag(flags, "paymentReference");
|
|
12667
|
+
const paymentDate = getFlag(flags, "paymentDate");
|
|
12668
|
+
const paymentAccount = getFlag(flags, "paymentAccount");
|
|
12669
|
+
const paymentProofPath = getFlag(flags, "paymentProof");
|
|
12670
|
+
if (!id || !paymentReference || !paymentDate || !paymentAccount)
|
|
12671
|
+
throw new Error("record-external-bill-payment requires <billId> --paymentReference --paymentDate YYYY-MM-DD --paymentAccount <QBO account ID> [--paymentProof receipt.pdf]");
|
|
12672
|
+
await recordExternalBillPayment({
|
|
12673
|
+
id,
|
|
12674
|
+
paymentReference,
|
|
12675
|
+
paymentDate,
|
|
12676
|
+
paymentAccount,
|
|
12677
|
+
paymentProofPath
|
|
12678
|
+
});
|
|
12679
|
+
break;
|
|
12680
|
+
}
|
|
12272
12681
|
case "patch-bill-payment": {
|
|
12273
12682
|
const id = positional[0];
|
|
12274
12683
|
if (!id)
|