@usagekit/core 0.8.0 → 0.9.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/dist/contracts.d.ts +21 -5
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/providers.d.ts +79 -5
- package/dist/providers.js +82 -1
- package/package.json +1 -1
package/dist/contracts.d.ts
CHANGED
|
@@ -269,8 +269,8 @@ export type ReserveResult = ValidationFailure | {
|
|
|
269
269
|
operation: Operation;
|
|
270
270
|
}
|
|
271
271
|
/**
|
|
272
|
-
* The connection's tracking policy is passthrough for this operation, or the catalog
|
|
273
|
-
* free: counted with state passthrough, never reserved. The caller may forward the call.
|
|
272
|
+
* The BYOK connection's tracking policy is passthrough for this operation, or the catalog
|
|
273
|
+
* marks it free: counted with state passthrough, never reserved. The caller may forward the call.
|
|
274
274
|
*/
|
|
275
275
|
| {
|
|
276
276
|
outcome: "passthrough";
|
|
@@ -289,15 +289,30 @@ export type ReserveResult = ValidationFailure | {
|
|
|
289
289
|
/** Where a reserve estimate came from, in resolution order. */
|
|
290
290
|
export type EstimateSource = "manual" | "measured" | "list" | "unknown";
|
|
291
291
|
/**
|
|
292
|
-
* Meter reserve input.
|
|
293
|
-
*
|
|
292
|
+
* Meter reserve input. For BYOK with a catalog, estimate may be omitted: the Meter resolves it
|
|
293
|
+
* for the connection and request options. Billable platform-funded calls require a host
|
|
294
|
+
* customer-price estimate; catalog-declared nonbillable calls are counted without a reservation.
|
|
295
|
+
* Store reservations always receive a ReserveInput.
|
|
294
296
|
*/
|
|
295
297
|
export type MeterReserveInput = Omit<ReserveInput, "estimate"> & {
|
|
296
|
-
/**
|
|
298
|
+
/** BYOK may omit for catalog resolution; billable platform calls require host customer pricing. */
|
|
297
299
|
estimate?: readonly Quantity[];
|
|
298
300
|
/** Request options that move the price, read from the request by the descriptor. */
|
|
299
301
|
options?: Readonly<Record<string, string>>;
|
|
300
302
|
};
|
|
303
|
+
/** Authorized advisory estimate. No operation identity or accounting side effects. */
|
|
304
|
+
export type MeterQuoteInput = Omit<MeterReserveInput, "operationId" | "reservationTtlMs" | "correlationId" | "parentOperationId">;
|
|
305
|
+
export type MeterQuote = {
|
|
306
|
+
quantities: readonly Quantity[];
|
|
307
|
+
/** Matches stored reservation provenance; omitted when none was supplied or resolved. */
|
|
308
|
+
estimateSource?: EstimateSource;
|
|
309
|
+
providerPriceVersion?: string;
|
|
310
|
+
checkedAt?: string;
|
|
311
|
+
sampleSize?: number;
|
|
312
|
+
tracking: "metered" | "passthrough" | "unpriced";
|
|
313
|
+
budgets: readonly BudgetStatus[];
|
|
314
|
+
asOf: string;
|
|
315
|
+
};
|
|
301
316
|
/**
|
|
302
317
|
* Non-operation outcomes, counted per state and never reserved. Metered requests are the
|
|
303
318
|
* operations themselves and are not counted here.
|
|
@@ -636,6 +651,7 @@ export type ExpireReservationsResult = ValidationFailure | {
|
|
|
636
651
|
* Admission sweeps at most one default batch. Maintenance owns the remaining expired backlog.
|
|
637
652
|
*/
|
|
638
653
|
export interface Meter {
|
|
654
|
+
quote(access: AccessContext, input: MeterQuoteInput): Promise<ReadResult<MeterQuote>>;
|
|
639
655
|
importBilling(access: AccessContext, input: BillingImportInput): Promise<BillingImportResult | {
|
|
640
656
|
outcome: "forbidden";
|
|
641
657
|
}>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { Money, Quantity, AccessCredential, Scope, FundingSource, Surface, Source, BudgetWindow, BudgetScope, Budget, BudgetAlert, BudgetAlertCrossed, LifecycleState, Certainty, Measurement, Cost, Receipt, ReserveInput, ReserveResult, Operation, OperationRef, OperationCommand, Lease, DispatchGrant, DispatchIntentInput, LeaseRenewalInput, LeaseRenewal, RecoveryClaimInput, RecoveryClaim, Authority, SettleInput, ReleaseInput, CorrectionInput, SettleResult, ReleaseResult, UsageScope, AccessContext, UsageQuery, UsageRow, UsagePage, OperationsQuery, OperationsPage, DefinedBudgetsQuery, ApplicableBudgetsQuery, BudgetStatus, ReadResult, AllowanceExceeded, Meter, MeterReserveInput, EstimateSource, RequestState, CountRequestInput, CountRequestResult, RequestCountsQuery, RequestCountRow, RequestCountsPage, } from "./contracts.js";
|
|
1
|
+
export type { Money, Quantity, AccessCredential, Scope, FundingSource, Surface, Source, BudgetWindow, BudgetScope, Budget, BudgetAlert, BudgetAlertCrossed, LifecycleState, Certainty, Measurement, Cost, Receipt, ReserveInput, ReserveResult, Operation, OperationRef, OperationCommand, Lease, DispatchGrant, DispatchIntentInput, LeaseRenewalInput, LeaseRenewal, RecoveryClaimInput, RecoveryClaim, Authority, SettleInput, ReleaseInput, CorrectionInput, SettleResult, ReleaseResult, UsageScope, AccessContext, UsageQuery, UsageRow, UsagePage, OperationsQuery, OperationsPage, DefinedBudgetsQuery, ApplicableBudgetsQuery, BudgetStatus, ReadResult, AllowanceExceeded, Meter, MeterReserveInput, MeterQuoteInput, MeterQuote, EstimateSource, RequestState, CountRequestInput, CountRequestResult, RequestCountsQuery, RequestCountRow, RequestCountsPage, } from "./contracts.js";
|
|
2
2
|
export { requestStates } from "./policy.js";
|
|
3
3
|
export { resolveWindow } from "./windows.js";
|
|
4
4
|
export type { ResolvedWindow } from "./windows.js";
|
|
@@ -8,6 +8,6 @@ export type { ValidationFailure, AdmissionPolicy, BudgetOwner } from "./contract
|
|
|
8
8
|
export { defaultBudgetOrder, sourcesOf, surfaceMatches, tagPattern, normalizeTags, } from "./policy.js";
|
|
9
9
|
export type { ExpireReservationsInput, ExpireReservationsResult } from "./contracts.js";
|
|
10
10
|
export { decodeMeterJson } from "./wire.js";
|
|
11
|
-
export type { ProviderId, OperationId, ProviderAuth, BillingUnit, ProviderPlan, PriceRow, RequestMatch, ProviderOperation, BalanceProbe, PriceListSource, BillingExport, ProviderDescriptor, BalanceSnapshot, BillingLine, DescriptorProblem, PricingCatalog, ConnectionPolicy, TrackingPolicy, MeasuredPrice, } from "./providers.js";
|
|
12
|
-
export { validateDescriptor } from "./providers.js";
|
|
11
|
+
export type { ProviderId, OperationId, ProviderAuth, BillingUnit, ProviderPlan, PriceRow, PriceBasis, PriceCalculation, RequestMatch, ProviderOperation, BalanceProbe, PriceListSource, BillingExport, ProviderDescriptor, BalanceSnapshot, BillingLine, DescriptorProblem, PricingCatalog, ConnectionPolicy, TrackingPolicy, MeasuredPrice, } from "./providers.js";
|
|
12
|
+
export { validateDescriptor, validateConnectionPolicy } from "./providers.js";
|
|
13
13
|
export type { ImportAttribution, BillingImportInput, ReconciliationEntry, BillingImportRecord, BillingImportResult, BillingImportsQuery, BillingImportsPage, BillingOperationChange, } from "./billing.js";
|
package/dist/index.js
CHANGED
|
@@ -4,4 +4,4 @@ export { fromDecimalString, toDecimalString, formatMoney, add as addMoney, sub a
|
|
|
4
4
|
export { normalizeScale, add as addQuantity, compare as compareQuantity, format as formatQuantity, } from "./quantity.js";
|
|
5
5
|
export { defaultBudgetOrder, sourcesOf, surfaceMatches, tagPattern, normalizeTags, } from "./policy.js";
|
|
6
6
|
export { decodeMeterJson } from "./wire.js";
|
|
7
|
-
export { validateDescriptor } from "./providers.js";
|
|
7
|
+
export { validateDescriptor, validateConnectionPolicy } from "./providers.js";
|
package/dist/providers.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EstimateSource, Money, Quantity } from "./contracts.js";
|
|
1
|
+
import type { EstimateSource, FundingSource, Money, Quantity, ValidationFailure } from "./contracts.js";
|
|
2
2
|
/**
|
|
3
3
|
* Provider descriptors: data that tells usagekit how to price, route, meter and reconcile one
|
|
4
4
|
* provider. Descriptors live in `@usagekit/providers`; the core contract does not depend on
|
|
@@ -36,11 +36,41 @@ export type ProviderPlan = {
|
|
|
36
36
|
value: bigint;
|
|
37
37
|
};
|
|
38
38
|
allowanceMode: "hard" | "soft";
|
|
39
|
+
/** Provider subscription data; never a per-receipt debit. */
|
|
40
|
+
subscription?: {
|
|
41
|
+
period: "month";
|
|
42
|
+
fee: Money;
|
|
43
|
+
source: string;
|
|
44
|
+
checkedAt: string;
|
|
45
|
+
};
|
|
39
46
|
};
|
|
40
47
|
/**
|
|
41
|
-
*
|
|
42
|
-
* the public list; those are manual prices on the connection.
|
|
48
|
+
* Overrides default to a whole-call price; item overrides replace a formula coefficient.
|
|
43
49
|
*/
|
|
50
|
+
export type PriceBasis = "call" | "item";
|
|
51
|
+
export type PriceCalculation = {
|
|
52
|
+
kind: "option_multiplier";
|
|
53
|
+
multiplierOption: {
|
|
54
|
+
key: string;
|
|
55
|
+
value: string;
|
|
56
|
+
factor: string;
|
|
57
|
+
};
|
|
58
|
+
} | {
|
|
59
|
+
kind: "additional_pages";
|
|
60
|
+
depthOption: string;
|
|
61
|
+
pageSize: number;
|
|
62
|
+
additionalPageFactor: string;
|
|
63
|
+
} | {
|
|
64
|
+
kind: "base_plus_items";
|
|
65
|
+
itemCountOption: string;
|
|
66
|
+
perItem: string;
|
|
67
|
+
multiplierOption?: {
|
|
68
|
+
key: string;
|
|
69
|
+
value: string;
|
|
70
|
+
factor: string;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
/** One public list price, with optional request calculation metadata. */
|
|
44
74
|
export type PriceRow = {
|
|
45
75
|
/** ISO date. */
|
|
46
76
|
validFrom: string;
|
|
@@ -54,6 +84,7 @@ export type PriceRow = {
|
|
|
54
84
|
unit: BillingUnit;
|
|
55
85
|
/** Exact nonnegative decimal in that unit. */
|
|
56
86
|
perUnit: string;
|
|
87
|
+
calculation?: PriceCalculation;
|
|
57
88
|
/** URL of the public price source. */
|
|
58
89
|
source: string;
|
|
59
90
|
/** ISO date of the last check against the source. */
|
|
@@ -76,6 +107,19 @@ export type ProviderOperation = {
|
|
|
76
107
|
match: readonly RequestMatch[];
|
|
77
108
|
/** Options that move the price; values are extracted from the request by `options`. */
|
|
78
109
|
optionKeys?: readonly string[];
|
|
110
|
+
/** Exhaustive documented request fields; unclassified fields cannot be safely priced. */
|
|
111
|
+
requestPricing?: {
|
|
112
|
+
source: string;
|
|
113
|
+
formula: string;
|
|
114
|
+
priceFields: Readonly<Record<string, {
|
|
115
|
+
formula: string;
|
|
116
|
+
/** A supplied opaque value can prevent any defensible upper bound. */
|
|
117
|
+
unpricedWhen?: "present" | "true";
|
|
118
|
+
/** Documented item count when this field is omitted. */
|
|
119
|
+
defaultCount?: number;
|
|
120
|
+
}>>;
|
|
121
|
+
neutralFields: readonly string[];
|
|
122
|
+
};
|
|
79
123
|
/**
|
|
80
124
|
* Cost is known from the response body, only after a later call, or never per call.
|
|
81
125
|
* response: settle on stream close. deferred: settle with unknown cost, keep pending, resolve
|
|
@@ -132,6 +176,8 @@ export type ProviderDescriptor = {
|
|
|
132
176
|
/** Origin and optional base path, for example "https://api.example.com". */
|
|
133
177
|
upstream: string;
|
|
134
178
|
auth: ProviderAuth;
|
|
179
|
+
/** Pricing and matching see body/query only when declared; method and URL path are always available. */
|
|
180
|
+
pricingInputs: readonly ("body" | "query")[];
|
|
135
181
|
stripRequestHeaders?: readonly string[];
|
|
136
182
|
billing: {
|
|
137
183
|
unit: BillingUnit;
|
|
@@ -176,26 +222,39 @@ export type BillingLine = {
|
|
|
176
222
|
to: string;
|
|
177
223
|
};
|
|
178
224
|
};
|
|
179
|
-
/** Per-
|
|
225
|
+
/** Per-operation tracking; passthrough opts BYOK out of spend enforcement only. */
|
|
180
226
|
export type TrackingPolicy = "metered" | "passthrough";
|
|
181
227
|
/** Median of a connection's settled receipts for one operation, with its sample size. */
|
|
182
228
|
export type MeasuredPrice = (operation: OperationId) => {
|
|
183
229
|
quantity: Quantity;
|
|
184
230
|
samples: number;
|
|
231
|
+
basis?: PriceBasis;
|
|
232
|
+
checkedAt?: string;
|
|
185
233
|
} | undefined;
|
|
186
234
|
/**
|
|
187
|
-
* Connection data a host resolves for the Meter: provider, selected plan, manual
|
|
235
|
+
* Connection data a host resolves for the Meter: provider, selected plan, BYOK manual prices,
|
|
188
236
|
* measured prices and tracking policy, all keyed by OperationId. overage is true when the host
|
|
189
237
|
* knows the plan allowance is exhausted.
|
|
190
238
|
*/
|
|
191
239
|
export type ConnectionPolicy = {
|
|
192
240
|
provider: ProviderId;
|
|
241
|
+
/** Actual connection funding, derived by the host from trusted server state. */
|
|
242
|
+
fundingSource: FundingSource;
|
|
193
243
|
plan?: string;
|
|
244
|
+
/** BYOK only: estimates the user's own provider cost, including genuine zero. */
|
|
194
245
|
manualPrices?: Readonly<Record<OperationId, Quantity>>;
|
|
246
|
+
manualPriceBasis?: Readonly<Record<OperationId, PriceBasis>>;
|
|
195
247
|
measured?: MeasuredPrice;
|
|
196
248
|
tracking?: Readonly<Record<OperationId, TrackingPolicy>>;
|
|
197
249
|
overage?: boolean;
|
|
198
250
|
};
|
|
251
|
+
/** Validate raw connection settings before saving. Manual prices are BYOK-only. */
|
|
252
|
+
export declare function validateConnectionPolicy(policy: {
|
|
253
|
+
fundingSource: FundingSource;
|
|
254
|
+
tracking?: unknown;
|
|
255
|
+
manualPrices?: unknown;
|
|
256
|
+
manualPriceBasis?: unknown;
|
|
257
|
+
}, operations: readonly Pick<ProviderOperation, "id" | "billable">[]): ValidationFailure | null;
|
|
199
258
|
/**
|
|
200
259
|
* What the Meter needs from a provider catalog. `@usagekit/providers` createCatalog satisfies
|
|
201
260
|
* it; the Meter does not depend on that package. operationsOf lists the operations of an
|
|
@@ -208,10 +267,17 @@ export type PricingCatalog = {
|
|
|
208
267
|
quantities: Quantity[];
|
|
209
268
|
source: EstimateSource;
|
|
210
269
|
priceVersion?: string;
|
|
270
|
+
checkedAt?: string;
|
|
271
|
+
sampleSize?: number;
|
|
211
272
|
};
|
|
212
273
|
operationsOf(providerId: ProviderId): readonly ProviderOperation[];
|
|
213
274
|
};
|
|
214
275
|
export type DescriptorProblem = {
|
|
276
|
+
kind: "invalid_pricing_inputs";
|
|
277
|
+
} | {
|
|
278
|
+
kind: "undeclared_query_input";
|
|
279
|
+
operation: OperationId;
|
|
280
|
+
} | {
|
|
215
281
|
kind: "duplicate_operation";
|
|
216
282
|
operation: OperationId;
|
|
217
283
|
} | {
|
|
@@ -236,6 +302,14 @@ export type DescriptorProblem = {
|
|
|
236
302
|
kind: "invalid_price";
|
|
237
303
|
row: number;
|
|
238
304
|
field: "perUnit" | "validFrom" | "checkedAt" | "source";
|
|
305
|
+
} | {
|
|
306
|
+
kind: "invalid_calculation";
|
|
307
|
+
row: number;
|
|
308
|
+
field: string;
|
|
309
|
+
} | {
|
|
310
|
+
kind: "invalid_subscription";
|
|
311
|
+
plan: string;
|
|
312
|
+
field: string;
|
|
239
313
|
} | {
|
|
240
314
|
kind: "unknown_probe_operation";
|
|
241
315
|
operation: OperationId;
|
package/dist/providers.js
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
/** Validate raw connection settings before saving. Manual prices are BYOK-only. */
|
|
2
|
+
export function validateConnectionPolicy(policy, operations) {
|
|
3
|
+
const invalid = (field, reason) => ({
|
|
4
|
+
outcome: "invalid",
|
|
5
|
+
field,
|
|
6
|
+
reason,
|
|
7
|
+
});
|
|
8
|
+
if (policy.fundingSource === "platform") {
|
|
9
|
+
for (const field of ["manualPrices", "manualPriceBasis"]) {
|
|
10
|
+
const value = policy[field];
|
|
11
|
+
const absent = value == null ||
|
|
12
|
+
(typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0);
|
|
13
|
+
if (!absent)
|
|
14
|
+
return invalid(field, "manual provider prices are BYOK-only");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const { tracking } = policy;
|
|
18
|
+
if (tracking === undefined)
|
|
19
|
+
return null;
|
|
20
|
+
if (!tracking || typeof tracking !== "object" || Array.isArray(tracking))
|
|
21
|
+
return invalid("tracking", "must be an operation-to-tracking map");
|
|
22
|
+
for (const [id, mode] of Object.entries(tracking)) {
|
|
23
|
+
const operation = operations.find((entry) => entry.id === id);
|
|
24
|
+
if (policy.fundingSource === "platform" &&
|
|
25
|
+
operation?.billable !== false &&
|
|
26
|
+
(mode === "passthrough" || mode === "unpriced"))
|
|
27
|
+
return invalid(`tracking.${id}`, "billable platform operations must be metered; credits charges cannot be opted out");
|
|
28
|
+
if (!operation)
|
|
29
|
+
return invalid(`tracking.${id}`, "unknown operation");
|
|
30
|
+
if (mode !== "metered" && mode !== "passthrough")
|
|
31
|
+
return invalid(`tracking.${id}`, "must be metered or passthrough");
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
1
35
|
const isoDate = (text) => /^\d{4}-\d{2}-\d{2}/.test(text) && Number.isFinite(Date.parse(text));
|
|
2
36
|
/**
|
|
3
37
|
* Structural problems of a descriptor, in a stable order: operations, plans, prices, probe,
|
|
@@ -5,8 +39,15 @@ const isoDate = (text) => /^\d{4}-\d{2}-\d{2}/.test(text) && Number.isFinite(Dat
|
|
|
5
39
|
*/
|
|
6
40
|
export function validateDescriptor(d) {
|
|
7
41
|
const problems = [];
|
|
42
|
+
if (!Array.isArray(d.pricingInputs) ||
|
|
43
|
+
d.pricingInputs.some((part) => part !== "body" && part !== "query") ||
|
|
44
|
+
new Set(d.pricingInputs).size !== d.pricingInputs.length)
|
|
45
|
+
problems.push({ kind: "invalid_pricing_inputs" });
|
|
8
46
|
const operations = new Map();
|
|
9
47
|
for (const op of d.operations) {
|
|
48
|
+
if (!(Array.isArray(d.pricingInputs) && d.pricingInputs.includes("query")) &&
|
|
49
|
+
op.match.some((rule) => Object.keys(rule.query ?? {}).length > 0))
|
|
50
|
+
problems.push({ kind: "undeclared_query_input", operation: op.id });
|
|
10
51
|
if (operations.has(op.id))
|
|
11
52
|
problems.push({ kind: "duplicate_operation", operation: op.id });
|
|
12
53
|
else
|
|
@@ -20,6 +61,17 @@ export function validateDescriptor(d) {
|
|
|
20
61
|
if (plans.has(plan.id))
|
|
21
62
|
problems.push({ kind: "duplicate_plan", plan: plan.id });
|
|
22
63
|
plans.add(plan.id);
|
|
64
|
+
const fee = plan.subscription;
|
|
65
|
+
if (fee) {
|
|
66
|
+
for (const [field, valid] of Object.entries({
|
|
67
|
+
period: fee.period === "month",
|
|
68
|
+
fee: fee.fee.currency === "USD" && typeof fee.fee.units === "bigint" && fee.fee.units >= 0n,
|
|
69
|
+
source: Boolean(fee.source.trim()),
|
|
70
|
+
checkedAt: isoDate(fee.checkedAt),
|
|
71
|
+
}))
|
|
72
|
+
if (!valid)
|
|
73
|
+
problems.push({ kind: "invalid_subscription", plan: plan.id, field });
|
|
74
|
+
}
|
|
23
75
|
}
|
|
24
76
|
d.prices.forEach((row, index) => {
|
|
25
77
|
const op = operations.get(row.operation);
|
|
@@ -30,8 +82,37 @@ export function validateDescriptor(d) {
|
|
|
30
82
|
for (const key of Object.keys(row.option ?? {}))
|
|
31
83
|
if (op && !(op.optionKeys ?? []).includes(key))
|
|
32
84
|
problems.push({ kind: "unknown_price_option", row: index, option: key });
|
|
33
|
-
if (!/^\d+(\.\d
|
|
85
|
+
if (!/^\d+(\.\d{1,18})?$/.test(row.perUnit))
|
|
34
86
|
problems.push({ kind: "invalid_price", row: index, field: "perUnit" });
|
|
87
|
+
const c = row.calculation;
|
|
88
|
+
if (c) {
|
|
89
|
+
const decimal = (x) => /^\d+(\.\d{1,18})?$/.test(x);
|
|
90
|
+
const option = (key) => {
|
|
91
|
+
if (op && !(op.optionKeys ?? []).includes(key))
|
|
92
|
+
problems.push({ kind: "unknown_price_option", row: index, option: key });
|
|
93
|
+
};
|
|
94
|
+
if (c.kind === "additional_pages") {
|
|
95
|
+
option(c.depthOption);
|
|
96
|
+
if (!Number.isSafeInteger(c.pageSize) || c.pageSize < 1)
|
|
97
|
+
problems.push({ kind: "invalid_calculation", row: index, field: "pageSize" });
|
|
98
|
+
if (!decimal(c.additionalPageFactor))
|
|
99
|
+
problems.push({ kind: "invalid_calculation", row: index, field: "additionalPageFactor" });
|
|
100
|
+
}
|
|
101
|
+
else if (c.kind === "base_plus_items") {
|
|
102
|
+
option(c.itemCountOption);
|
|
103
|
+
if (!decimal(c.perItem))
|
|
104
|
+
problems.push({ kind: "invalid_calculation", row: index, field: "perItem" });
|
|
105
|
+
}
|
|
106
|
+
else if (c.kind !== "option_multiplier")
|
|
107
|
+
problems.push({ kind: "invalid_calculation", row: index, field: "kind" });
|
|
108
|
+
if (c.kind === "option_multiplier" && !c.multiplierOption)
|
|
109
|
+
problems.push({ kind: "invalid_calculation", row: index, field: "multiplierOption" });
|
|
110
|
+
if ((c.kind === "base_plus_items" || c.kind === "option_multiplier") && c.multiplierOption) {
|
|
111
|
+
option(c.multiplierOption.key);
|
|
112
|
+
if (!c.multiplierOption.value.trim() || !decimal(c.multiplierOption.factor))
|
|
113
|
+
problems.push({ kind: "invalid_calculation", row: index, field: "multiplierOption" });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
35
116
|
if (!isoDate(row.validFrom))
|
|
36
117
|
problems.push({ kind: "invalid_price", row: index, field: "validFrom" });
|
|
37
118
|
if (!isoDate(row.checkedAt))
|