@classytic/primitives 0.19.0 → 0.20.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +906 -751
  2. package/README.md +160 -159
  3. package/dist/approval.d.mts +49 -10
  4. package/dist/approval.mjs +44 -10
  5. package/dist/bank-transaction.d.mts +1 -1
  6. package/dist/cadence-BFQVC1Qp.d.mts +107 -0
  7. package/dist/cadence.d.mts +1 -92
  8. package/dist/cadence.mjs +133 -4
  9. package/dist/calendar-B6fm-FbR.d.mts +46 -0
  10. package/dist/calendar.d.mts +1 -45
  11. package/dist/canonical.mjs +17 -0
  12. package/dist/context.d.mts +28 -1
  13. package/dist/currency.d.mts +2 -2
  14. package/dist/currency.mjs +134 -19
  15. package/dist/event-infra.d.mts +1 -34
  16. package/dist/event-infra.mjs +16 -46
  17. package/dist/idempotency.d.mts +253 -0
  18. package/dist/idempotency.mjs +226 -0
  19. package/dist/memory-outbox.d.mts +64 -0
  20. package/dist/memory-outbox.mjs +172 -0
  21. package/dist/monetization.d.mts +1 -1
  22. package/dist/money-DPo9fLBi.d.mts +347 -0
  23. package/dist/money.d.mts +1 -140
  24. package/dist/money.mjs +6 -15
  25. package/dist/outbox.d.mts +49 -1
  26. package/dist/outbox.mjs +64 -1
  27. package/dist/payment-events.d.mts +132 -4
  28. package/dist/payment-events.mjs +22 -2
  29. package/dist/payment-gateway.d.mts +210 -7
  30. package/dist/payment-gateway.mjs +18 -1
  31. package/dist/period.d.mts +138 -7
  32. package/dist/period.mjs +316 -5
  33. package/dist/person-parse.d.mts +82 -0
  34. package/dist/person-parse.mjs +109 -0
  35. package/dist/proration.d.mts +2 -2
  36. package/dist/proration.mjs +18 -6
  37. package/dist/sla-policy.d.mts +1 -1
  38. package/dist/state-machine.mjs +13 -0
  39. package/dist/suspension.d.mts +1 -1
  40. package/dist/timezone-tlG7ygwx.d.mts +114 -0
  41. package/dist/timezone.d.mts +2 -100
  42. package/dist/timezone.mjs +38 -3
  43. package/dist/unit-cost-rate.d.mts +1 -2
  44. package/package.json +244 -232
  45. package/dist/currency-BCJnJBlb.d.mts +0 -119
@@ -1,4 +1,4 @@
1
- import { Money } from "./money.mjs";
1
+ import { n as Money } from "./money-DPo9fLBi.mjs";
2
2
  import { PaymentMethodKind } from "./payment-method-kind.mjs";
3
3
 
4
4
  //#region src/money/payment-gateway.d.ts
@@ -25,7 +25,7 @@ interface ProviderCapabilities {
25
25
  * Arguments to `provider.createIntent(...)`. Engine constructs this
26
26
  * from the host's checkout payload and the resolved currency. The
27
27
  * `metadata` blob is opaque pass-through — providers MAY forward it to
28
- * the upstream gateway (Stripe attaches it to the PaymentIntent), but
28
+ * the upstream gateway (Stripe attaches it to its own PaymentIntent), but
29
29
  * MUST NOT mutate or interpret it.
30
30
  */
31
31
  interface CreateIntentParams {
@@ -49,7 +49,7 @@ interface CreateIntentParams {
49
49
  * Stripe's PaymentIntent status because every other gateway maps cleanly
50
50
  * INTO this set; gateways that lack a state simply never emit it.
51
51
  */
52
- type PaymentIntentStatus = 'requires_payment_method' | 'requires_confirmation' | 'requires_action' | 'processing' | 'requires_capture' | 'canceled' | 'succeeded';
52
+ type ProviderIntentStatus = 'requires_payment_method' | 'requires_confirmation' | 'requires_action' | 'processing' | 'requires_capture' | 'canceled' | 'succeeded';
53
53
  /** Closed set of refund lifecycle states the engine recognises. */
54
54
  type RefundResultStatus = 'pending' | 'requires_action' | 'succeeded' | 'failed' | 'canceled';
55
55
  /**
@@ -58,7 +58,17 @@ type RefundResultStatus = 'pending' | 'requires_action' | 'succeeded' | 'failed'
58
58
  * `gateway` block so subsequent `verify` / webhook lookups can find
59
59
  * the row.
60
60
  */
61
- interface PaymentIntent {
61
+ /**
62
+ * A PROVIDER-side intent reference — what the gateway calls this payment.
63
+ *
64
+ * Named `ProviderIntent`, not `PaymentIntent`, because the domain aggregate that models the
65
+ * customer's obligation is also called a payment intent, and one name cannot span both layers.
66
+ * This is the narrow thing: a gateway-scoped id, the provider's own status mapped onto our closed
67
+ * union, and the amount it locked in. It belongs to ONE attempt against ONE provider account.
68
+ *
69
+ * See `spine/docs/payments-architecture.md` §3.3.
70
+ */
71
+ interface ProviderIntent {
62
72
  /** Unique gateway-scoped id for this intent (`pi_xxx`, `ch_xxx`, etc.). */
63
73
  id: string;
64
74
  /** Lowercase gateway name (`'stripe'`, `'razorpay'`, `'sslcommerz'`, `'bkash'`, `'manual'`). */
@@ -67,7 +77,7 @@ interface PaymentIntent {
67
77
  * Lifecycle status — closed union the engine state-machines branch on.
68
78
  * Adapters MUST map provider-native strings into this set.
69
79
  */
70
- status: PaymentIntentStatus;
80
+ status: ProviderIntentStatus;
71
81
  /** Amount + currency that the intent locks in. */
72
82
  amount: Money;
73
83
  /**
@@ -104,7 +114,7 @@ interface PaymentResult {
104
114
  /** Lowercase gateway name. */
105
115
  provider: string;
106
116
  /**
107
- * Closed status set — narrower than `PaymentIntent.status` because
117
+ * Closed status set — narrower than `ProviderIntent.status` because
108
118
  * verification has settled the lifecycle.
109
119
  *
110
120
  * - `'succeeded'` — payment captured, funds owed to merchant.
@@ -185,5 +195,198 @@ interface WebhookEvent {
185
195
  /** Raw provider payload — audit only. */
186
196
  raw?: unknown;
187
197
  }
198
+ /**
199
+ * The contract a payment provider must satisfy — declared HERE, in primitives, so an
200
+ * adapter can be written against it with **no runtime dependency on the engine**.
201
+ *
202
+ * ## Why this moved
203
+ *
204
+ * `@classytic/revenue-manual` imported the abstract `PaymentProvider` CLASS from
205
+ * `@classytic/revenue` — a runtime value, not a type. That coupled every adapter to the
206
+ * engine package: an adapter could not be versioned or published independently, and the
207
+ * dependency ran adapter → engine while the engine also registers adapters.
208
+ *
209
+ * This is the same correction made for `OutboxStore`: primitives owns the CONTRACT, the
210
+ * engine adapts to it, and a kit or a third party can implement it knowing only this file.
211
+ *
212
+ * ## Extending vs implementing
213
+ *
214
+ * `revenue`'s abstract `PaymentProvider` class now *implements* this interface and stays
215
+ * available for the config plumbing it provides (`config`, `defaultCurrency`). Existing
216
+ * adapters that `extends PaymentProvider` are unaffected. New adapters should `implement
217
+ * PaymentProviderPort` directly and depend only on primitives.
218
+ *
219
+ * See `spine/docs/payments-architecture.md` §3.4.1 and phase 1.
220
+ */
221
+ /**
222
+ * How reconciliation asks a provider about a specific REFUND. The refund is identified
223
+ * by the original `paymentId` plus the refund's `idempotencyKey` (the value the provider
224
+ * deduped on); `refundRef` is the gateway refund id when the engine observed one.
225
+ */
226
+ interface RefundStatusQuery {
227
+ paymentId: string;
228
+ idempotencyKey: string;
229
+ refundRef?: string;
230
+ }
231
+ interface PaymentProviderPort {
232
+ /** Stable lowercase provider name — the registry key. */
233
+ readonly name: string;
234
+ /**
235
+ * Create a provider-side intent for an amount.
236
+ *
237
+ * `command` is REQUIRED, not optional. Creating an intent is side-effecting upstream: a
238
+ * timed-out create can leave a live intent at the gateway with no local record, and a
239
+ * retry without a stable idempotency key produces a SECOND one. Making the envelope
240
+ * mandatory is what stops an adapter quietly omitting it.
241
+ */
242
+ createIntent(params: CreateIntentParams, command: PaymentCommandContext): Promise<ProviderIntent>;
243
+ /**
244
+ * Confirm an intent actually succeeded at the provider.
245
+ *
246
+ * A provider that CANNOT know — a stateless manual provider, for instance — must not
247
+ * answer `succeeded`. Reporting success for something it never observed is how a
248
+ * reconciliation path gets a false positive.
249
+ */
250
+ verifyPayment(intentId: string, command?: PaymentCommandContext): Promise<PaymentResult>;
251
+ /**
252
+ * Current provider-side status. Same honesty requirement as `verifyPayment`.
253
+ *
254
+ * `command` is OPTIONAL here and on `verifyPayment`: both are reads, so there is nothing
255
+ * to make idempotent. It is accepted purely for correlation across our logs and the
256
+ * provider's.
257
+ */
258
+ getStatus(intentId: string, command?: PaymentCommandContext): Promise<PaymentResult>;
259
+ /**
260
+ * Reverse a payment, in full or in part.
261
+ *
262
+ * `command` is REQUIRED and carries the idempotency key. It used to be an optional field
263
+ * inside `options`, which made the single most dangerous operation on this port the one
264
+ * where deduplication was easiest to forget. Providers whose gateway supports an
265
+ * idempotency key MUST forward it; those without may ignore it, and the engine still
266
+ * guards with its own claim and a unique index.
267
+ */
268
+ refund(paymentId: string, amount: number | null | undefined, command: PaymentCommandContext, options?: {
269
+ reason?: string;
270
+ }): Promise<RefundResult>;
271
+ /**
272
+ * Current status of a REFUND (not the payment). OPTIONAL and feature-detected: only
273
+ * a provider that can look a refund up implements it. Reconciliation MUST use this,
274
+ * never `getStatus` (which reports the PAYMENT) — asking the payment about a refund
275
+ * misclassifies a successful charge as a successful reversal. The query carries the
276
+ * original `paymentId` and the refund's `idempotencyKey` (and `refundRef` when we
277
+ * observed one) so the provider can find the specific reversal it deduped. A provider
278
+ * without this cannot be auto-reconciled — the engine then RETAINS the reservation.
279
+ */
280
+ getRefundStatus?(query: RefundStatusQuery, command?: PaymentCommandContext): Promise<PaymentResult>;
281
+ handleWebhook(payload: unknown, headers?: Record<string, string>): Promise<WebhookEvent>;
282
+ /**
283
+ * Reject a forged webhook. Returning `true` unconditionally is only acceptable for a
284
+ * provider with no webhook transport at all (manual/dev).
285
+ */
286
+ verifyWebhookSignature(payload: unknown, signature: string): boolean;
287
+ /** Declared capabilities. Callers gate on these rather than probing by failing. */
288
+ getCapabilities(): ProviderCapabilities;
289
+ }
290
+ /**
291
+ * `setDefaultCurrency` is deliberately NOT on the port.
292
+ *
293
+ * A default currency is ENGINE configuration, not part of the provider execution contract,
294
+ * and every `Money` already carries its own currency. Requiring the mutator would make each
295
+ * adapter stateful and awkward to share across accounts — the same instance cannot then
296
+ * serve two accounts with different defaults.
297
+ *
298
+ * Adapters that want the convenience expose it and the registry feature-detects it; the
299
+ * rest simply read the currency off the amount they were handed.
300
+ */
301
+ interface DefaultCurrencyAware {
302
+ setDefaultCurrency(currency: string): void;
303
+ }
304
+ /**
305
+ * The envelope every money-changing provider command carries.
306
+ *
307
+ * Idempotency used to be supplied on `refund` alone. It belongs on every command: a
308
+ * retried authorize or capture double-charges just as readily as a retried refund, and the
309
+ * retry is usually the client's, not ours.
310
+ *
311
+ * Adapters whose gateway supports an idempotency key MUST forward it. The engine still keeps
312
+ * its own atomic claim/result store, because not every provider offers one and a provider's
313
+ * word is not a guarantee we control.
314
+ */
315
+ interface PaymentCommandContext {
316
+ /**
317
+ * Caller-supplied, stable across retries of the SAME logical operation. Two different
318
+ * operations must never share one; one operation retried must never change it.
319
+ */
320
+ idempotencyKey: string;
321
+ /** Correlates this attempt across our logs, the provider's, and the audit trail. */
322
+ requestId: string;
323
+ /** Our reference for the thing being paid for — order number, invoice number. */
324
+ merchantReference: string;
325
+ /**
326
+ * Branch / tenant scope. OPTIONAL: a revenue engine can run unscoped
327
+ * (single-tenant / company-global), where there is no branch id to carry —
328
+ * so this is absent rather than an empty or asserted string. Scoped engines
329
+ * always set it; consumers that require scope must check, not cast.
330
+ */
331
+ organizationId?: string;
332
+ /** Cancels an in-flight command (a cashier abandoning a terminal prompt, a shutdown). */
333
+ signal?: AbortSignal;
334
+ }
335
+ /** Why a provider said no. A DECISION, distinct from an unanswered question. */
336
+ interface PaymentDecline {
337
+ /** Normalized reason. Adapters map their vendor's vocabulary onto this. */
338
+ reason: 'insufficient_funds' | 'card_declined' | 'expired' | 'invalid_details' | 'limit_exceeded' | 'duplicate' | 'not_permitted' | 'provider_error' | 'other';
339
+ /** The provider's own code and message, preserved verbatim for support and reconciliation. */
340
+ providerCode?: string;
341
+ providerMessage?: string;
342
+ /** Whether trying again could plausibly succeed. `false` for a hard decline. */
343
+ retryable: boolean;
344
+ }
345
+ /**
346
+ * The outcome of a provider command. **Three-valued, and the third one is the point.**
347
+ *
348
+ * A timeout is NOT a failure. When a capture request times out, the money may well have
349
+ * moved — we simply did not hear back. Treating that as `declined` and retrying is the
350
+ * canonical way to double-charge a customer, and it is invisible until reconciliation.
351
+ *
352
+ * `unknown` also covers a provider that legitimately cannot answer: a stateless manual
353
+ * provider asked for the status of a payment it never stored. Today's two-valued shape
354
+ * forces such a provider to invent an answer, and `revenue-manual` invented `succeeded`.
355
+ *
356
+ * **The only correct response to `unknown` is to reconcile status before acting** — never
357
+ * a blind retry.
358
+ */
359
+ type ProviderCommandResult<T> = {
360
+ outcome: 'confirmed';
361
+ value: T;
362
+ } | {
363
+ outcome: 'declined';
364
+ error: PaymentDecline;
365
+ } | {
366
+ outcome: 'unknown'; /** Whatever handle we have for asking again later. */
367
+ providerReference?: string;
368
+ /**
369
+ * NORMALIZED reason we do not know — never a raw exception message.
370
+ *
371
+ * Vendor errors routinely embed request URLs, tokens, account identifiers and
372
+ * fragments of the request body. This value is persisted on the transaction, returned
373
+ * to callers and read by operators, so it must be safe to store and display. The raw
374
+ * error belongs in a redacted diagnostic log, not here.
375
+ */
376
+ causeCode?: ProviderUnknownCause;
377
+ };
378
+ /** Why an outcome went unobserved. Coarse on purpose — it drives handling, not diagnosis. */
379
+ type ProviderUnknownCause = 'timeout' | 'aborted' | 'network' | 'status_unavailable' | 'unclassified';
380
+ /**
381
+ * Thrown by a provider that cannot answer a status query, rather than guessing.
382
+ *
383
+ * Classified as `unknown`. The engine's correct reaction is to consult the stored record:
384
+ * for a manual method there is no external money-movement authority, so the record IS the
385
+ * authority (see `spine/docs/payments-architecture.md` §1).
386
+ */
387
+ declare class ProviderStatusUnavailableError extends Error {
388
+ readonly providerName: string;
389
+ constructor(providerName: string, message?: string);
390
+ }
188
391
  //#endregion
189
- export { CreateIntentParams, PaymentIntent, PaymentIntentStatus, PaymentResult, ProviderCapabilities, RefundResult, RefundResultStatus, WebhookEvent };
392
+ export { CreateIntentParams, DefaultCurrencyAware, PaymentCommandContext, PaymentDecline, PaymentProviderPort, PaymentResult, ProviderCapabilities, ProviderCommandResult, ProviderIntent, ProviderIntentStatus, ProviderStatusUnavailableError, ProviderUnknownCause, RefundResult, RefundResultStatus, RefundStatusQuery, WebhookEvent };
@@ -1 +1,18 @@
1
- export {};
1
+ //#region src/money/payment-gateway.ts
2
+ /**
3
+ * Thrown by a provider that cannot answer a status query, rather than guessing.
4
+ *
5
+ * Classified as `unknown`. The engine's correct reaction is to consult the stored record:
6
+ * for a manual method there is no external money-movement authority, so the record IS the
7
+ * authority (see `spine/docs/payments-architecture.md` §1).
8
+ */
9
+ var ProviderStatusUnavailableError = class extends Error {
10
+ providerName;
11
+ constructor(providerName, message) {
12
+ super(message ?? `provider "${providerName}" cannot report status for this payment — it holds no record of it. Read the stored transaction instead of asking the provider.`);
13
+ this.name = "ProviderStatusUnavailableError";
14
+ this.providerName = providerName;
15
+ }
16
+ };
17
+ //#endregion
18
+ export { ProviderStatusUnavailableError };
package/dist/period.d.mts CHANGED
@@ -1,15 +1,31 @@
1
+ import { t as CivilDate } from "./timezone-tlG7ygwx.mjs";
2
+
1
3
  //#region src/scheduling/period.d.ts
2
4
  /**
3
- * A closed date range `[start, end]`. Both endpoints inclusive — consumers
4
- * decide whether to treat them as `[start, end)` at the query layer.
5
+ * A date range `[start, end]`.
6
+ *
7
+ * The endpoints are documented as INCLUSIVE for hand-built ranges, and
8
+ * {@link isWithin} treats them that way. Every `resolve*` function in this
9
+ * module instead returns a HALF-OPEN `[start, end)` range — the shape a
10
+ * `{ $gte: start, $lt: end }` query wants, and the only shape that tiles
11
+ * exactly (one period's `end` IS the next one's `start`, so no document is
12
+ * counted twice and none falls in a gap). Compare against a resolved range
13
+ * with `>= start && < end`, NOT with `isWithin`, which would accept the first
14
+ * instant of the following period. {@link inclusiveEnd} converts when a
15
+ * `$lte` call site genuinely needs the last instant.
5
16
  */
6
17
  interface DateRange {
7
18
  start: Date;
8
19
  end: Date;
9
20
  }
10
21
  /**
11
- * A named accounting / reporting period. At least one of the resolution fields
12
- * (`month`, `quarter`) should be set, or `start`/`end` for ad-hoc ranges.
22
+ * A named accounting / reporting period.
23
+ *
24
+ * Exactly ONE resolution shape may be present: `month`, or `quarter`, or an
25
+ * ad-hoc `start`+`end` pair, or none of them (meaning the whole `year`).
26
+ * Combining them is rejected by {@link resolvePeriod} rather than silently
27
+ * preferring one — two possible answers to "which instants is this?" is the
28
+ * input you must not widen.
13
29
  */
14
30
  interface Period {
15
31
  /** Four-digit calendar year, e.g. 2026. */
@@ -18,14 +34,46 @@ interface Period {
18
34
  month?: number;
19
35
  /** 1..4. */
20
36
  quarter?: 1 | 2 | 3 | 4;
37
+ /**
38
+ * The IANA zone this period is DEFINED in — `'Asia/Dhaka'` for a BD filing
39
+ * month, `'America/New_York'` for a US one. Carried on the value so a period
40
+ * that travels (persisted on a filing, posted in a job payload, echoed in a
41
+ * report header) keeps the calendar it was computed against instead of
42
+ * inheriting whatever the reader's default happens to be.
43
+ *
44
+ * Optional because an ad-hoc `start`/`end` period needs no zone, and because
45
+ * a caller may supply the zone at resolve time. {@link resolvePeriod} throws
46
+ * when neither is available, and throws when the two DISAGREE.
47
+ */
48
+ timezone?: string;
21
49
  /** Ad-hoc override — useful when the period spans a non-calendar boundary. */
22
50
  start?: Date;
23
51
  end?: Date;
24
52
  }
53
+ /** Which resolution shape a {@link Period} carries. */
54
+ type PeriodGranularity = 'year' | 'quarter' | 'month' | 'custom';
55
+ declare class DateRangeError extends Error {
56
+ readonly name = "DateRangeError";
57
+ }
58
+ type PeriodErrorCode = /** Two resolution shapes at once (`month` + `quarter`, or either + `start`/`end`). */'AMBIGUOUS_PERIOD' /** `start` without `end`, or vice versa. */ | 'INCOMPLETE_RANGE' /** `year` / `month` / `quarter` outside its legal domain, or non-integer. */ | 'INVALID_FIELD' /** No zone on the period and none supplied by the caller. */ | 'MISSING_TIMEZONE' /** The period's own zone and the caller's zone disagree. */ | 'TIMEZONE_CONFLICT' /** A `'YYYY'` / `'YYYY-MM'` / `'YYYY-Qn'` label that could not be read. */ | 'INVALID_LABEL';
59
+ declare class PeriodError extends Error {
60
+ readonly name = "PeriodError";
61
+ readonly code: PeriodErrorCode;
62
+ constructor(code: PeriodErrorCode, message: string);
63
+ }
25
64
  declare function isDateRange(value: unknown): value is DateRange;
26
- /** True if `d` is inclusively within `range`. */
65
+ /**
66
+ * True if `d` is inclusively within `range`.
67
+ *
68
+ * @throws DateRangeError if `d` or either endpoint is an Invalid Date.
69
+ */
27
70
  declare function isWithin(d: Date, range: DateRange): boolean;
28
- /** Range duration in milliseconds. */
71
+ /**
72
+ * Range duration in milliseconds.
73
+ *
74
+ * @throws DateRangeError if either endpoint is an Invalid Date (which would
75
+ * otherwise return `NaN` and propagate silently into a duration total).
76
+ */
29
77
  declare function rangeDurationMs(range: DateRange): number;
30
78
  /**
31
79
  * True if two half-open ranges `[start, end)` overlap.
@@ -40,7 +88,90 @@ declare function rangeDurationMs(range: DateRange): number;
40
88
  * - `@classytic/order` booking overlap detection
41
89
  * - `@classytic/flow` reservation window collision
42
90
  * - promo / offer campaign window overlap checks
91
+ *
92
+ * @throws DateRangeError if either range carries an Invalid Date. For all three
93
+ * consumers above, `false` is the PERMISSIVE answer — "no conflict, go ahead
94
+ * and book" — so a range we cannot read must not be allowed to produce it.
43
95
  */
44
96
  declare function rangesOverlap(a: DateRange, b: DateRange): boolean;
97
+ /**
98
+ * The first instant of a local calendar day, in `timezone`. DST-exact — the
99
+ * civil-date round-trip resolves the offset AT local midnight, not at "now".
100
+ */
101
+ declare function dayStart(date: CivilDate | string, timezone: string): Date;
102
+ /** Half-open `[start, end)` range covering one local calendar day. */
103
+ declare function resolveDay(date: CivilDate | string, timezone: string): DateRange;
104
+ /**
105
+ * Half-open `[start, end)` range covering one local calendar month.
106
+ * `resolveMonth(2026, 8, 'Asia/Dhaka')` →
107
+ * `[2026-07-31T18:00Z, 2026-08-31T18:00Z)` — the DHAKA month, six hours off
108
+ * the UTC one at both ends.
109
+ */
110
+ declare function resolveMonth(year: number, month: number, timezone: string): DateRange;
111
+ /** Half-open `[start, end)` range covering one local calendar quarter. */
112
+ declare function resolveQuarter(year: number, quarter: 1 | 2 | 3 | 4, timezone: string): DateRange;
113
+ /** Half-open `[start, end)` range covering one local calendar year. */
114
+ declare function resolveYear(year: number, timezone: string): DateRange;
115
+ /**
116
+ * Half-open range spanning two local dates, INCLUSIVE of both days — what an
117
+ * operator means by "1st to 31st". The half-open end is therefore the start of
118
+ * the day AFTER `to`.
119
+ */
120
+ declare function resolveDateSpan(from: CivilDate | string, to: CivilDate | string, timezone: string): DateRange;
121
+ /** Which resolution shape this period carries. Throws when it carries two. */
122
+ declare function granularityOf(period: Period): PeriodGranularity;
123
+ /**
124
+ * Resolve the zone a period must be read in.
125
+ *
126
+ * `period.timezone` is the zone the period was DEFINED in; `timezone` is the
127
+ * caller's (usually the deployment default). Supplying both with different
128
+ * values is a bug — one of them is describing a different calendar than the
129
+ * data was computed against — so it throws rather than picking a winner.
130
+ */
131
+ declare function periodTimeZone(period: Period, timezone?: string): string;
132
+ /**
133
+ * `Period` → half-open `[start, end)` instants, in a business timezone.
134
+ *
135
+ * The single derivation every kernel, module and host should reach for.
136
+ *
137
+ * ```ts
138
+ * resolvePeriod({ year: 2026, month: 8 }, 'Asia/Dhaka');
139
+ * // → [2026-07-31T18:00:00Z, 2026-08-31T18:00:00Z)
140
+ * ```
141
+ *
142
+ * An ad-hoc period (`start` + `end`) needs no zone and is returned verbatim
143
+ * after an Invalid-Date check.
144
+ */
145
+ declare function resolvePeriod(period: Period, timezone?: string): DateRange;
146
+ /**
147
+ * The LAST instant of a half-open range — `end - 1ms`.
148
+ *
149
+ * For the `$lte` call sites that predate half-open ranges. Derived from the
150
+ * resolved end rather than `start + 24h - 1`, so it stays DST-exact.
151
+ */
152
+ declare function inclusiveEnd(range: DateRange): Date;
153
+ /**
154
+ * The period an INSTANT falls in, on the local calendar.
155
+ *
156
+ * The inverse of {@link resolvePeriod}, and the answer to "which filing month
157
+ * is this posting in?" — asked at local midnight ±6h, `getUTCMonth()` answers a
158
+ * different month. The returned period carries the zone it was derived in, so
159
+ * a later `resolvePeriod` cannot re-read it against a different calendar.
160
+ */
161
+ declare function periodOf(instant: Date, granularity: Exclude<PeriodGranularity, 'custom'>, timezone: string): Period;
162
+ /**
163
+ * Parse a period LABEL — `'2026'`, `'2026-08'`, `'2026-Q3'`.
164
+ *
165
+ * Case-sensitive on the `Q` and strictly anchored: a label this cannot read
166
+ * throws instead of degrading to the year it could see. A `'YYYY-MM-DD'` is
167
+ * rejected with a pointer to {@link resolveDay}, because a `Period` has no day
168
+ * field and quietly reading it as its month is a 30× widening of the range.
169
+ */
170
+ declare function parsePeriod(label: string, timezone?: string): Period;
171
+ /**
172
+ * `Period` → its canonical label. Round-trips through {@link parsePeriod}.
173
+ * Throws for an ad-hoc period, which has no label — format its endpoints.
174
+ */
175
+ declare function formatPeriod(period: Period): string;
45
176
  //#endregion
46
- export { DateRange, Period, isDateRange, isWithin, rangeDurationMs, rangesOverlap };
177
+ export { DateRange, DateRangeError, Period, PeriodError, PeriodErrorCode, PeriodGranularity, dayStart, formatPeriod, granularityOf, inclusiveEnd, isDateRange, isWithin, parsePeriod, periodOf, periodTimeZone, rangeDurationMs, rangesOverlap, resolveDateSpan, resolveDay, resolveMonth, resolvePeriod, resolveQuarter, resolveYear };